OAuth2 Token Revocation — RFC 7009 Token Revocation for Access and Refresh Tokens
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
- What does RFC 7009 define?
- Why should revocation return 200 for unknown tokens?
- How do you revoke JWT access tokens?
- What is the difference between access and refresh token revocation?
- How do you propagate revocations in a microservice architecture?
Answers:
- A standard HTTP endpoint for token revocation:
POST /revokewithtokenand optionaltoken_type_hint. - To prevent attackers from enumerating valid tokens by observing error responses.
- You cannot un-sign a JWT. Add the token's hash to a blacklist until its natural expiry.
- Access tokens are revoked immediately; refresh tokens are permanently invalidated (deleted or marked as used).
- 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
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