Token Revocation

Authentication Security Notes Jan 9, 2026 HTTP
oauth token security logout rfc-7009

Definition

What happens when a user clicks “Log out” in your app? Or when you realize a token might be compromised? Or when a user removes third-party app access from their account settings? You can’t just hope the token expires soon - you need to actively invalidate it. That’s where token revocation comes in.

Token Revocation (RFC 7009) is an OAuth 2.0 extension that provides a standardized endpoint for clients to notify the authorization server that a previously obtained token should be invalidated immediately. This applies to both access tokens and refresh tokens. When a token is revoked, it becomes immediately invalid even if it hasn’t reached its expiration time.

This is critical for security and user control. Without revocation, a stolen access token remains valid until expiration (potentially hours), and a stolen refresh token could be used to obtain new access tokens indefinitely. Revocation gives users and applications the ability to instantly cut off access, making it essential for logout flows, security incidents, and user-initiated access removal.

Example

Google Account Security: When you visit your Google Account security page and see a list of devices/apps with access to your account, clicking “Remove access” on any of them triggers token revocation. Google’s authorization server immediately invalidates all access and refresh tokens for that application, even if they were set to expire days from now.

GitHub OAuth Apps: When you revoke a GitHub OAuth app’s access from your settings page, GitHub calls its internal revocation endpoint. All access tokens for that app-user combination become invalid instantly. The next time the app tries to use the token to access your repos, it gets a 401 Unauthorized.

Corporate Security Incident: An employee reports their laptop stolen. The security team immediately revokes all OAuth tokens associated with that device. Even though the access tokens might have been valid for another hour and refresh tokens for another month, they’re all dead within seconds of revocation.

Banking App Logout: You finish checking your bank balance on a public computer and click “Log out.” The banking app calls the revocation endpoint for both the access token and refresh token. If you forgot to log out and someone else sits down at that computer, they can’t use the old tokens even if they’re still in browser memory.

Slack Workspace Security: A Slack admin notices suspicious API activity from a third-party integration. They revoke the OAuth token from Slack’s admin panel. The integration’s next API call fails immediately, and they must re-authorize to regain access.

Analogy

The Digital Key Deactivation: Imagine you gave a digital room key to a visitor that’s valid for 24 hours. But after 2 hours, you want them out. Token expiration is like waiting 22 more hours for the key to stop working. Token revocation is like immediately deactivating that specific key in the hotel’s central system - the physical card still exists, but it won’t unlock anything anymore.

The Concert Wristband Blacklist: You bought a concert wristband that’s valid all weekend. But you get kicked out for bad behavior. The wristband still looks valid and has tomorrow’s date on it, but security adds it to a blacklist. Token revocation is that blacklist - the wristband didn’t expire, but it’s explicitly marked as “do not allow entry.”

The Restaurant Reservation Cancellation: You made a reservation for 8 PM tonight, but you call at 6 PM to cancel. The reservation wasn’t expired (it’s still valid for 2 more hours), but you explicitly canceled it. When you show up later that night, the host says “Sorry, you canceled this reservation.” Token revocation is that explicit cancellation.

The Library Card Suspension: Your library card says it’s valid until 2027, but you rack up $100 in late fees. The library suspends your card - it’s not expired, but it’s been revoked. When you try to check out books, the scanner shows “ACCOUNT SUSPENDED” even though the physical card says it’s still valid.

Code Example

# Client-initiated revocation (user logout)
POST /oauth/revoke HTTP/1.1
Host: authorization-server.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW

token=45ghiukldjahdnhzdauz4a5za4az5za&
token_type_hint=access_token

# Successful revocation response
HTTP/1.1 200 OK
Content-Type: application/json

{
  "revoked": true
}

# Revoking a refresh token (also invalidates associated access tokens)
POST /oauth/revoke HTTP/1.1
Host: authorization-server.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW

token=fdb8fdbecf1d03855e3b8b55e3b8f&
token_type_hint=refresh_token

# If token is already invalid/revoked or doesn't exist
# Still returns 200 OK (RFC 7009 spec)
HTTP/1.1 200 OK

# Client-side implementation (React/JavaScript)
// Token revocation utility function
async function revokeToken(token, tokenType = 'access_token') {
  const clientId = 'your-client-id';
  const clientSecret = 'your-client-secret';

  try {
    const response = await fetch('https://auth.example.com/oauth/revoke', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Authorization': `Basic ${btoa(`${clientId}:${clientSecret}`)}`
      },
      body: new URLSearchParams({
        token: token,
        token_type_hint: tokenType
      })
    });

    // RFC 7009: endpoint MUST return 200 even if token is invalid
    if (response.status === 200) {
      console.log(`Token revoked successfully`);
      return true;
    } else {
      console.error(`Revocation failed with status: ${response.status}`);
      return false;
    }
  } catch (error) {
    console.error('Revocation request failed:', error);
    return false;
  }
}

// Logout flow with proper token cleanup
async function logout() {
  const accessToken = localStorage.getItem('access_token');
  const refreshToken = localStorage.getItem('refresh_token');

  // Revoke refresh token first (may cascade to access tokens)
  if (refreshToken) {
    await revokeToken(refreshToken, 'refresh_token');
  }

  // Then explicitly revoke access token
  if (accessToken) {
    await revokeToken(accessToken, 'access_token');
  }

  // Clear local storage
  localStorage.removeItem('access_token');
  localStorage.removeItem('refresh_token');
  localStorage.removeItem('user_info');

  // Redirect to login
  window.location.href = '/login';
}

// Server-side enforcement (Node.js/Express)
// Authorization server maintains revocation list
const revokedTokens = new Set(); // In production: use Redis or database

app.post('/oauth/revoke', authenticate, (req, res) => {
  const { token, token_type_hint } = req.body;

  if (!token) {
    // RFC 7009: invalid_request if token is missing
    return res.status(400).json({
      error: 'invalid_request',
      error_description: 'token parameter is required'
    });
  }

  // Validate that requester owns the token or is authorized
  // (check client_id matches token's client_id)
  const tokenInfo = getTokenInfo(token); // Your token lookup logic
  if (tokenInfo && tokenInfo.client_id !== req.client.id) {
    // RFC 7009: return 200 even for unauthorized attempts (don't leak info)
    return res.status(200).send();
  }

  // Add to revocation list
  revokedTokens.add(token);

  // If it's a refresh token, also revoke associated access tokens
  if (token_type_hint === 'refresh_token') {
    const associatedTokens = findAccessTokensByRefreshToken(token);
    associatedTokens.forEach(at => revokedTokens.add(at));
  }

  // Update database/cache
  db.tokens.update(
    { token: token },
    { $set: { revoked: true, revoked_at: new Date() } }
  );

  // RFC 7009: always return 200 OK
  res.status(200).send();
});

// Resource server checks revocation before processing requests
app.use('/api/*', async (req, res, next) => {
  const token = req.headers.authorization?.replace('Bearer ', '');

  if (!token) {
    return res.status(401).json({ error: 'Missing token' });
  }

  // Check if token is in revocation list
  if (revokedTokens.has(token)) {
    return res.status(401).json({
      error: 'Token has been revoked'
    });
  }

  // Optionally: introspect token for real-time revocation status
  const introspection = await introspectToken(token);
  if (!introspection.active) {
    return res.status(401).json({
      error: 'Token is not active'
    });
  }

  next();
});

Diagram

sequenceDiagram
    participant User
    participant Client
    participant AuthServer as Authorization Server
    participant ResourceServer as Resource Server
    participant RevocationDB as Revocation Database

    Note over User,Client: User clicks "Logout"

    User->>Client: Logout request

    Client->>AuthServer: POST /revoke
token={refresh_token}
token_type_hint=refresh_token AuthServer->>RevocationDB: Mark refresh token as revoked
Find associated access tokens RevocationDB-->>AuthServer: Tokens marked as revoked AuthServer->>RevocationDB: Add all tokens to
revocation blacklist AuthServer-->>Client: 200 OK Client->>Client: Clear local tokens
from storage Client-->>User: Redirect to login page Note over User,ResourceServer: Later: Attacker tries to use old token User->>ResourceServer: GET /api/data
Authorization: Bearer {old_token} ResourceServer->>AuthServer: Introspect token
or check revocation list AuthServer->>RevocationDB: Check if token revoked RevocationDB-->>AuthServer: Token found in
revocation list AuthServer-->>ResourceServer: {"active": false} ResourceServer-->>User: 401 Unauthorized
Token has been revoked

Security Notes

SECURITY NOTES

CRITICAL: Token revocation allows immediate access denial. Implement carefully for performance.

Revocation Methods:

  • Blacklist tokens: Maintain list of revoked tokens
  • Short lifetimes: Issue short-lived tokens (minutes/hours)
  • Revocation endpoint: Endpoint to revoke tokens
  • JTI tracking: Use JWT ID claim for revocation tracking
  • Distributed revocation: Share revocation list across servers

Revocation Protocol:

  • RFC 7009: Standard token revocation specification
  • POST request: Send token to revocation endpoint
  • Client authentication: Verify client identity
  • 204 response: Return 204 No Content on success
  • Idempotent: Revoking already-revoked token should succeed

Implementation Strategies:

  • Database blacklist: Store revoked tokens in database
  • Cache blacklist: Cache frequently revoked tokens
  • Periodic cleanup: Clean up expired revocations
  • Distributed cache: Use Redis for fast lookups
  • Bloom filter: Use Bloom filter for memory-efficient blacklist

Security Considerations:

  • Revocation delay: Immediate revocation requires real-time check
  • Cache inconsistency: Distributed caches may have stale data
  • DoS vector: Revocation endpoint can be DoS target
  • Rate limiting: Rate limit revocation requests
  • Monitoring: Monitor revocation endpoint health

Best Practices

  1. Revoke Refresh Tokens First: Always revoke refresh tokens before access tokens, as they have longer lifetimes
  2. Cascade Revocation: When revoking a refresh token, automatically revoke all associated access tokens
  3. Use Token IDs: Store and revoke by jti (JWT ID) claim rather than full token strings
  4. Implement Blacklisting: Maintain a distributed, fast-access revocation list (Redis, DynamoDB)
  5. Automatic Revocation Triggers: Revoke tokens on password change, account deletion, suspicious activity
  6. Client-Side Cleanup: After revocation, clear tokens from all local storage (localStorage, sessionStorage, cookies)
  7. Idempotent Revocation: Revoking an already-revoked token should succeed (return 200 OK)
  8. Provide User Control: Give users UI to view and revoke active sessions/tokens
  9. Audit Logging: Log all revocation events with context (who, what, when, why)
  10. Combine with Introspection: Use token introspection (RFC 7662) to check revocation status in real-time

Common Mistakes

Forgetting Associated Tokens: Revoking only the access token but not the refresh token, allowing the attacker to obtain new access tokens. Solution: Revoke the refresh token to cascade revocation.

No Client Authentication: Exposing the revocation endpoint without authentication, allowing anyone to revoke any token. Solution: Require client credentials.

Not Checking Revocation: Resource servers accept tokens without checking the revocation list, defeating the purpose. Solution: Integrate revocation checks via introspection or direct blacklist queries.

Information Leakage: Returning different error codes for “token doesn’t exist” vs “token revoked” vs “unauthorized.” Solution: Always return 200 OK per RFC 7009.

Poor Performance: Storing the revocation list in a slow database, causing latency on every introspection. Solution: Use in-memory cache (Redis) with database backup.

No TTL on Revocation Records: Keeping revoked token records forever, bloating storage. Solution: Set TTL to match maximum token lifetime.

Client-Side Only Revocation: Just deleting tokens from localStorage without calling the revocation endpoint. Solution: Always call /revoke before clearing local state.

Ignoring Logout: Not implementing proper logout flows that revoke tokens server-side. Solution: Logout must call revocation endpoint.

Standards & RFCs