How to Implement Refresh Token Rotation in Python
A mock auth service that issues, rotates, and validates refresh tokens, revoking old tokens on reuse to prevent replay attacks.
Python code
67 linesimport time
import hashlib
import secrets
from typing import Dict, Optional, Tuple
class MockTokenService:
"""Simulates refresh token rotation for a simple auth system."""
def __init__(self):
# Token hash -> (user_id, rotation_count, expires_at)
self._active_tokens: Dict[str, Tuple[str, int, int]] = {}
# Token hash -> (user_id, revoked_at) for audit of rotated tokens
self._rotated_tokens: Dict[str, Tuple[str, int]] = {}
self._token_ttl_seconds = 3600 # 1 hour
def _hash_token(self, token: str) -> str:
return hashlib.sha256(token.encode()).hexdigest()
def issue_refresh_token(self, user_id: str) -> str:
"""Issue a new refresh token for a user."""
token = secrets.token_urlsafe(32)
token_hash = self._hash_token(token)
self._active_tokens[token_hash] = (user_id, 0, time.time() + self._token_ttl_seconds)
return token
def rotate_refresh_token(self, old_token: str) -> Optional[str]:
"""Rotate a refresh token: revoke old, issue new one."""
old_hash = self._hash_token(old_token)
token_data = self._active_tokens.pop(old_hash, None)
if not token_data:
return None # Invalid or already revoked
user_id, rotation_count, expires_at = token_data
if time.time() > expires_at:
return None # Expired
# Record the rotated token for audit
self._rotated_tokens[old_hash] = (user_id, int(time.time()))
return self.issue_refresh_token(user_id)
def validate_token(self, token: str) -> Optional[Tuple[str, int]]:
"""Return (user_id, rotation_count) if token is valid, else None."""
token_hash = self._hash_token(token)
token_data = self._active_tokens.get(token_hash)
if not token_data or time.time() > token_data[2]:
return None
return (token_data[0], token_data[1])
if __name__ == "__main__":
service = MockTokenService()
# Issue initial token
token = service.issue_refresh_token("user-123")
print(f"Issued token: {token[:20]}...")
print(f"Valid initially: {service.validate_token(token) is not None}")
# Rotate it
rotated = service.rotate_refresh_token(token)
print(f"Rotated token: {rotated[:20]}...")
print(f"Old token now invalid: {service.validate_token(token) is None}")
print(f"New token valid: {service.validate_token(rotated) is not None}")
# Attempt reuse of old token (should fail)
reuse_attempt = service.rotate_refresh_token(token)
print(f"Reuse detected (returns None): {reuse_attempt is None}")
Output
Issued token: xK3s9fT2qLm8ZpR4vNcW1bH6yJdA5uE7...
Valid initially: True
Rotated token: YpQ7wE4rT6yUoI2aSdF9gHjKlZxVc0bN...
Old token now invalid: True
New token valid: True
Reuse detected (returns None): True
How it works
The secrets.token_urlsafe(32) call generates a cryptographically secure random token, and hashlib.sha256 hashes it before storage so raw tokens never touch the database. In rotate_refresh_token, the old token is popped from _active_tokens first, which atomically revokes it; a subsequent reuse attempt finds nothing and returns None. Expiry is checked before rotation to reject stale tokens. Tracking rotated tokens in _rotated_tokens provides an audit trail for security monitoring.
Common mistakes
- Storing raw token strings in memory instead of hashes
- Forgetting to check token expiration before rotation
- Allowing the same token to be used more than once after rotation
Variations
- Persist tokens and their hashes in Redis instead of an in-memory dict
- Add a device or IP binding to the token data for additional security
Real-world use cases
- OAuth2/OIDC providers rotating refresh tokens on every API call to minimize theft window.
- Mobile apps silently refreshing sessions in the background using rotate-on-refresh to detect potential session hijacking.
- CI/CD systems issuing short-lived deployment credentials and rotating them with each pipeline run.
Sponsored
More from Auth & security at scale
- ACME LetsEncrypt Mock Challenge Server in Python medium
- AES GCM encryption and decryption in Python medium
- Build a Mock OIDC Userinfo Endpoint in Python with Flask easy
- ChaCha20-Poly1305 mock in Python medium
- ECDH key agreement in Python with cryptography medium
- Enforce TLS 1.2 Minimum in Python easy
Keep learning
Related tutorials and quizzes for this topic.