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.

Medium Python 3.9+ Aug 9, 2026 Auth & security at scale 11 views 0 copies

Python code

67 lines
Python 3.9+
import 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

stdout
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

  1. Persist tokens and their hashes in Redis instead of an in-memory dict
  2. 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

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Auth & security at scale

Related tutorials and quizzes for this topic.