Skip to content

OAuth2 Token Revocation — RFC 7009 Token Revocation for Access and Refresh Tokens

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about OAuth2 Token Revocation. We cover key concepts, practical examples, and best practices to help you master this topic.

OAuth2 token revocation (RFC 7009) defines a standard endpoint for clients to revoke access and refresh tokens, enabling immediate invalidation of compromised or unwanted tokens.

What You'll Learn

  • RFC 7009 revocation endpoint implementation
  • Revocation strategies for opaque vs JWT tokens
  • Client-initiated vs authorization-server-initiated revocation
  • Handling revocation in Distributed Systems
  • Revocation event propagation and auditing

Why It Matters

Token revocation is the primary mechanism for responding to security incidents. When a user logs out or a device is compromised, the revocation endpoint immediately invalidates tokens. DodaTech's auth server handles 50K+ revocation requests daily, with sub-second propagation across all services.

Real-World Use

A security analyst's laptop is reported stolen. The admin triggers token revocation for all tokens issued to that user. The revocation endpoint invalidates the user's refresh token and broadcasts revocation events to all resource servers, who blacklist the tokens within seconds.

sequenceDiagram
    participant User as Security Admin
    participant Auth as Authorization Server
    participant TokenDB as Token Store
    participant RS1 as Resource Server 1
    participant RS2 as Resource Server 2
    participant Audit as Audit Log

    User->>Auth: POST /revoke (token_hint)
    Auth->>TokenDB: Invalidate token
    Auth-->>TokenDB: Update token status
    Auth->>RS1: Broadcast revocation event
    Auth->>RS2: Broadcast revocation event
    RS1->>RS1: Add token to blacklist
    RS2->>RS2: Add token to blacklist
    Auth->>Audit: Log revocation event
    Auth-->>User: 200 OK

Code Examples

Example 1: RFC 7009 Revocation Endpoint

from flask import Flask, request, jsonify
from datetime import datetime, timezone

app = Flask(__name__)

@app.route('/revoke', methods=['POST'])
def revoke_token():
    """RFC 7009 token revocation endpoint."""
    token = request.form.get('token')
    token_type_hint = request.form.get('token_type_hint', 'access_token')
    client_id = authenticate_client(request)

    if not token:
        return jsonify({'error': 'invalid_request'}), 400

    # Attempt revocation regardless of which client owns the token
    # RFC 7009: always return 200 to prevent token enumeration
    revoke_access_token(token, client_id)

    if token_type_hint == 'refresh_token':
        revoke_refresh_token(token, client_id)

    # Always return 200 OK per spec
    return jsonify({'result': 'ok'}), 200

def revoke_access_token(token, requesting_client):
    """Revoke an access token."""
    token_data = find_access_token(token)
    if not token_data:
        return  # Token unknown — silently ignore

    # Validate client is authorized to revoke
    # The token owner or the authorization server can always revoke
    if not can_revoke(token_data, requesting_client):
        return

    # Revoke the token
    if token_data['type'] == 'jwt':
        # JWT tokens: add to blacklist until expiry
        blacklist_token(token, token_data['exp'])
    else:
        # Opaque tokens: delete or mark as revoked
        mark_token_revoked(token)

    # Log revocation
    log_revocation({
        'token_hash': hash_token(token),
        'client_id': requesting_client,
        'token_owner': token_data.get('user_id'),
        'timestamp': datetime.now(timezone.utc).isoformat(),
        'reason': 'user_initiated'
    })

Example 2: Client-Side Token Revocation

import requests

class OAuth2Client:
    def __init__(self, client_id, client_secret, auth_url):
        self.client_id = client_id
        self.client_secret = client_secret
        self.auth_url = auth_url

    def revoke_token(self, token, token_type_hint='access_token'):
        """Revoke a token at the authorization server."""
        response = requests.post(
            f'{self.auth_url}/revoke',
            data={
                'token': token,
                'token_type_hint': token_type_hint,
                'client_id': self.client_id,
                'client_secret': self.client_secret
            }
        )

        if response.status_code == 200:
            print(f"Token revoked successfully")
            # Clear local storage
            self.clear_local_tokens()
        else:
            print(f"Revocation failed: {response.status_code}")
        return response.status_code == 200

    def logout(self):
        """Full logout: revoke both access and refresh tokens."""
        self.revoke_token(self.access_token, 'access_token')
        self.revoke_token(self.refresh_token, 'refresh_token')
        self.access_token = None
        self.refresh_token = None
        print("Logged out — all tokens revoked")

# User logout
client = OAuth2Client(CLIENT_ID, CLIENT_SECRET, 'https://auth.dodatech.com')
client.logout()

Example 3: Distributed Revocation via Event Bus

import redis
import json

class DistributedRevocation:
    def __init__(self, redis_client):
        self.redis = redis_client
        self.revocation_channel = 'token:revocations'

    def publish_revocation(self, token_hash, expires_at):
        """Publish revocation event to all subscribers."""
        event = {
            'token_hash': token_hash,
            'expires_at': expires_at.isoformat(),
            'published_at': datetime.now(timezone.utc).isoformat()
        }
        self.redis.publish(self.revocation_channel, json.dumps(event))
        print(f"Published revocation for token hash: {token_hash[:16]}...")

    def subscribe_and_blacklist(self):
        """Subscribe to revocation events and update local blacklist."""
        pubsub = self.redis.pubsub()
        pubsub.subscribe(self.revocation_channel)

        for message in pubsub.listen():
            if message['type'] != 'message':
                continue

            event = json.loads(message['data'])
            token_hash = event['token_hash']
            expires_at = datetime.fromisoformat(event['expires_at'])

            # Add to local blacklist with TTL matching token expiry
            blacklist_key = f"blacklist:{token_hash}"
            self.redis.setex(blacklist_key, expires_at, 'revoked')
            print(f"Blacklisted token {token_hash[:16]}... until {expires_at}")

    def is_revoked(self, token_hash):
        """Check if a token is in the distributed blacklist."""
        return self.redis.exists(f"blacklist:{token_hash}")

# Usage on each resource server
revoker = DistributedRevocation(redis_client)
# Start background listener
threading.Thread(target=revoker.subscribe_and_blacklist, daemon=True).start()

# Check tokens before accepting
if revoker.is_revoked(compute_hash(access_token)):
    return "Token revoked", 401

Common Mistakes

1. Returning Error for Unknown Tokens

RFC 7009 requires returning 200 OK even for unknown tokens to prevent token enumeration attacks.

2. Not Validating Client Authorization

Only the token owner or its client should be able to revoke a token. Anonymous revocation enables DoS attacks.

3. Ignoring JWT Blacklist TTL

JWTs remain valid until their natural expiry. The blacklist entry must live as long as the token would have.

4. No Distributed Revocation for Microservices

If you have multiple resource servers, use a shared event bus or blacklist to propagate revocations.

5. Not Logging Revocations

Every revocation should be logged for security auditing. Include who revoked it, when, and which token.

Practice Questions

  1. What does RFC 7009 define?
  2. Why should revocation return 200 for unknown tokens?
  3. How do you revoke JWT access tokens?
  4. What is the difference between access and refresh token revocation?
  5. How do you propagate revocations in a microservice architecture?

Answers:

  1. A standard HTTP endpoint for token revocation: POST /revoke with token and optional token_type_hint.
  2. To prevent attackers from enumerating valid tokens by observing error responses.
  3. You cannot un-sign a JWT. Add the token's hash to a blacklist until its natural expiry.
  4. Access tokens are revoked immediately; refresh tokens are permanently invalidated (deleted or marked as used).
  5. Use a message queue (Redis Pub/Sub, Kafka, RabbitMQ) to broadcast revocation events to all services.

Challenge: Build a complete revocation system with RFC 7009 endpoint, JWT blacklisting, distributed revocation via Redis, and an audit log. Test by revoking a token and verifying it is rejected across multiple services.

FAQ

Can I revoke a token without knowing the refresh token?

: Yes. Revoke by the access token. The authorization server can look up the associated refresh token.

How long should a JWT blacklist entry live?

: As long as the token's remaining TTL. Use Redis EXPIRE or set the TTL to token_exp - current_time.

What is `token_type_hint`?

: An optional parameter that tells the server whether the token is an access or refresh token, optimizing lookup.

Does revocation notify the resource owner?

: Not automatically, but you can send email notifications for suspicious revocation events.

Can third-party apps revoke their own tokens?

: Yes, and they should on logout. The client uses the revocation endpoint to clean up.

What's Next

Combine revocation with {{< ilink "OAuth" "OAuth2 Token Exchange" }} and {{< ilink "OAuth" "OAuth2 JWT" }} for a complete token lifecycle management system.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro