Stackvora.tech

Implement Refresh Token Rotation Safely

Implementing a robust token-based authentication system requires balancing security with user experience. Short-lived access tokens mitigate the impact of…

Implementing a robust token-based authentication system requires balancing security with user experience. Short-lived access tokens mitigate the impact of token compromise, but necessitate frequent re-authentication. Refresh tokens solve this by allowing clients to obtain new access tokens without requiring user credentials, but introduce their own security considerations. Refresh token rotation is a critical mechanism to enhance the security of this process.

This article details the implementation of refresh token rotation, including architectural considerations, flow diagrams, and best practices for server-side storage and revocation. We will focus on stateless access tokens (e.g., JWTs) and stateful refresh tokens.

The Token Rotation Flow Explained

The core principle of refresh token rotation is that each time a client uses a refresh token to obtain a new access token, the old refresh token is immediately invalidated, and a new refresh token is issued along with the new access token. This creates a "rolling window" where only the most recently issued refresh token for a given session is valid. If an attacker compromises a refresh token, its subsequent use by the legitimate client will cause the attacker's (now invalidated) token to be rejected, alerting the system to a potential breach.

The typical flow proceeds as follows:

  1. Initial Authentication: User logs in with credentials. Server validates credentials and issues a short-lived access token (e.g., 15 minutes validity) and a long-lived refresh token (e.g., 7-30 days validity). Both are sent to the client. The refresh token is stored server-side, typically in a database, associated with the user and client/device identifier.
  2. Access Token Usage: Client uses the access token to access protected resources.
  3. Access Token Expiration: When the access token expires, the client attempts to refresh it.
  4. Token Refresh Request: Client sends the refresh token to the server's token refresh endpoint.
  5. Server-Side Validation and Rotation:
    • Server validates the refresh token against its stored record (e.g., checks if it exists and is not expired/revoked).
    • Crucially, if valid, the old refresh token is immediately marked as invalidated/deleted from the store.
    • A new access token and a new refresh token are generated.
    • The new refresh token is stored server-side, replacing the old one, associated with the same user and client/device.
    • The new token pair is sent back to the client.
  6. Client Continues Usage: Client receives and uses the new token pair.

This mechanism ensures that if an attacker intercepts a refresh token, its first use by the attacker will render the legitimate user's refresh token invalid for subsequent requests, or vice-versa. This "one-time use" property for refresh tokens is key.

Server-Side Refresh Token Storage and Association

Refresh tokens are stateful and must be stored securely server-side. Storing them in a database allows for robust tracking, revocation, and association with specific user sessions or devices.

Database Schema Example

A typical table structure might look like this:

CREATE TABLE refresh_tokens (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id),
    token_hash VARCHAR(255) NOT NULL UNIQUE, -- Store hash, not plaintext token
    expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    is_revoked BOOLEAN DEFAULT FALSE,
    device_info TEXT, -- Optional: client-provided device identifier/details
    client_id VARCHAR(64) -- Optional: for multi-client applications (e.g., OAuth 2.0 clients)
);

CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
CREATE INDEX idx_refresh_tokens_expires_at ON refresh_tokens(expires_at);
  • id: A unique identifier for the refresh token record.
  • user_id: Foreign key linking to the user account.
  • token_hash: Crucially, store a cryptographically secure hash of the refresh token, not the plaintext token itself. Use algorithms like Argon2, bcrypt, or scrypt. This protects against database breaches. The client-sent refresh token is hashed and compared to this stored hash.
  • expires_at: The expiration timestamp for the refresh token. This should be longer than access tokens (e.g., 7-30 days).
  • is_revoked: A flag to manually revoke a token without waiting for expiration.
  • device_info/client_id: Optional fields to associate a token with a specific device or client application, enabling targeted revocation (e.g., "log out of all devices" or "log out of this device").

Generating Refresh Tokens

Refresh tokens themselves should be long, unpredictable, random strings (e.g., UUIDv4 or cryptographically secure random bytes encoded in Base64). They do not need to be JWTs, as their validity is determined by server-side storage.

# Example in Python for generating a refresh token
import secrets
import base64

# Generate 32 cryptographically secure random bytes
refresh_token_bytes = secrets.token_bytes(32)
# Encode to a URL-safe Base64 string
refresh_token = base64.urlsafe_b64encode(refresh_token_bytes).decode('utf-8').rstrip('=')
# Example output: "h_Xo0y6-p-z-A_q9k_j-f-Q_x-t_e-w-v-u-s-r-q-p-o-n-m-l-k-j-i-h-g-f-e-d-c-b-a" (length ~43 chars)

Compromise Detection and "Family" Revocation

Refresh token rotation significantly improves the detection of compromised tokens. If a client attempts to use an already invalidated refresh token (i.e., one that has been rotated out), this indicates a potential compromise. In such a scenario, the system should:

  1. Reject the request immediately.
  2. Invalidate the entire "family" of refresh tokens associated with that user and device. A "family" refers to the sequence of refresh tokens issued for a single user session. This means marking all active refresh tokens for that user/device as revoked in the database.
  3. Force re-authentication for the user. The user should be prompted to log in again with their credentials, effectively starting a new token family.
  4. Optionally, notify the user. An email or in-app notification can inform the user of suspicious activity and the need to re-authenticate.

This "drastic" response is necessary because if an invalidated token is being used, it implies an attacker has gained access to a refresh token that the legitimate client has already rotated away from. This is a strong signal of compromise.

Client-Side Handling and Storage

Clients must securely store refresh tokens. While access tokens can often reside in memory for their short lifespan, refresh tokens need persistence across application restarts or browser sessions. Common storage mechanisms include:

  • Web Applications:
    • HTTP-only, Secure Cookies: The most secure option for browsers. Cookies are automatically sent with requests to the same domain, preventing JavaScript access and reducing XSS risk.
    • Local Storage/Session Storage: Less secure due to XSS vulnerability, as JavaScript can access these. Not recommended for refresh tokens.
  • Mobile Applications:
    • Platform-specific secure storage: Keychain on iOS, KeyStore on Android. These are designed for sensitive data and are generally the most secure options.
  • Desktop Applications:
    • OS-provided credential managers: Windows Credential Manager, macOS Keychain.
    • Encrypted configuration files: Less ideal but sometimes used.

When using HTTP-only cookies, the refresh token exchange typically involves the server setting a new cookie with the updated refresh token on refresh. The client doesn't directly handle the refresh token's value after the initial login.

Trade-offs and Considerations

Increased Server-Side State

Refresh token rotation requires persistent server-side storage for refresh tokens, making them stateful. This contrasts with purely stateless JWT-based access token systems where no server-side state is required post-issuance for access tokens. This additional state adds operational overhead (database management, scaling) but is a necessary security trade-off.

Database Latency and Availability

Each refresh request necessitates a database lookup and update (invalidate old, store new). This introduces potential database latency and makes the authentication system dependent on database availability. Implement robust database connection pooling and consider read replicas for performance if refresh requests are very high.

Managing Concurrent Refresh Requests

If a client sends multiple refresh requests concurrently (e.g., due to race conditions or retries), and the first request succeeds in rotating the token, subsequent requests using the old (now invalidated) refresh token will trigger the compromise detection logic. To mitigate this:

  • Client-side queuing: Ensure clients don't send multiple refresh requests in quick succession.
  • Grace period/Token Reuse Window: Implement a short grace period (e.g., 5-10 seconds) where an invalidated refresh token is still temporarily accepted if it immediately precedes its successor in the rotation chain. This makes sense for systems under heavy load or with slow networks but slightly weakens the immediate compromise detection. This can be implemented by adding a previous_token_hash field to your database schema.

Common Pitfalls

  • Storing plaintext refresh tokens: Always hash refresh tokens before storing them in the database. A database breach should not expose valid refresh tokens.
  • Not revoking the old refresh token immediately: The "one-time use" property is fundamental. Delaying invalidation leaves a window for attack.
  • Failing to implement compromise detection ("family" revocation): If an invalid refresh token is used, merely rejecting it isn't enough. It's a strong signal of compromise that requires broader action.
  • Improper client-side storage: Storing refresh tokens in browser local storage or other insecure locations greatly increases the risk of XSS attacks leading to token theft. Prefer HTTP-only, secure cookies for web.
  • Insufficient refresh token expiration: While long-lived, they shouldn't be eternal. A maximum lifespan (e.g., 90 days) forces re-authentication periodically, even if rotation is ongoing.
  • Lack of refresh token cleanup: Regularly purge expired and revoked refresh tokens from your database to maintain performance and reduce data footprint.

Back to the knowledge base · Ask the AI assistant