JWT Refresh Token Rotation: Implementation Guide
A practical guide to JWT refresh tokens — token rotation, expiration strategies, secure storage, and preventing replay attacks.
Why Refresh Tokens?
JWT access tokens have a fundamental limitation: they expire. Short expiration times (15-60 minutes) limit the damage if a token is stolen, but require the user to re-authenticate frequently. Long expiration times are convenient but increase security risk. Refresh tokens solve this by providing a long-lived credential that can request new short-lived access tokens without requiring the user to log in again.
The refresh token pattern improves security through separation of concerns. Access tokens are sent with every API request and have a short lifespan. Refresh tokens are stored securely and used only to obtain new access tokens. If an access token is stolen, its short expiration limits the window of vulnerability. If a refresh token is stolen, the original can be rotated and invalidated.
Our JWT Expiration Checker helps you inspect both access and refresh tokens to verify their claims, expiration times, and structure. This is invaluable during development when you need to debug token lifecycle issues or verify that your authentication flow is working correctly.
Access Token vs Refresh Token Architecture
The standard JWT authentication flow uses two tokens with different lifetimes and purposes. The access token is short-lived (typically 15 minutes to 1 hour) and contains claims about the user's identity and permissions. It is sent in the Authorization header of every API request. The refresh token is long-lived (typically 7-30 days) and is used exclusively to obtain new access tokens.
When the access token expires, the client sends the refresh token to a dedicated endpoint (POST /auth/refresh) which validates the refresh token and returns a new access token. This happens transparently to the user — the client intercepts 401 responses, refreshes the token, and retries the original request. The user only needs to log in when the refresh token itself expires.
The key architectural decision is where to store each token. Access tokens can be stored in memory (JavaScript variable) or in an httpOnly cookie. Refresh tokens should always be stored in an httpOnly cookie with Secure and SameSite=Strict flags. Never store refresh tokens in localStorage — they are accessible to any JavaScript running on your domain, making them vulnerable to XSS attacks.
Refresh Token Rotation Strategy
Refresh token rotation is a security pattern where each refresh request invalidates the old refresh token and issues a new one. This means a refresh token can only be used once. If an attacker steals a refresh token and the legitimate user also uses it, one of them will attempt to use an already-rotated token, triggering a security alert.
Implementation of rotation requires a database or cache to track active refresh tokens. When a refresh request arrives, look up the token, verify it's still valid, issue a new access token AND a new refresh token, then invalidate the old refresh token by removing it from the database. If a rotated token is presented again, this indicates token theft — revoke all tokens for that user and require re-authentication.
Replay detection is the companion to rotation. Maintain a family of tokens — a chain linking each new token back to its original. If a token from outside the current family is presented, the entire family is compromised. This prevents an attacker who steals a single token from maintaining access indefinitely. Our JWT Decoder helps you inspect the claims in your tokens to verify family identifiers and rotation status.
Common Refresh Token Implementation Mistakes
The most common mistake is storing refresh tokens in the same database table as access tokens without distinguishing their purpose. Refresh tokens need different storage considerations — they must be revocable, support rotation, and have configurable lifetimes independent of access token policies. Always use separate storage or at minimum a type field to distinguish them.
Another frequent error is not validating the refresh token's association with the user. When a refresh request comes in, verify that the token belongs to the requesting user (match sub claim against authenticated user ID). Also validate that the token hasn't been revoked due to password change, account suspension, or administrative action.
Failing to implement absolute expiration is another critical mistake. Even with rotation, refresh tokens should have a hard maximum lifetime (typically 30-90 days) after which the user must re-authenticate regardless of rotation activity. This limits the window of exposure if an attacker maintains access through a compromised family of tokens. Use our JWT Expiration Checker to verify your token expiration policies during development and testing.
JWT Refresh Token Best Practices
Use httpOnly, Secure, SameSite=Strict cookies for refresh token storage. This prevents XSS attacks from reading the token and CSRF attacks from using it. Set the cookie path to /auth/refresh so it is only sent to the refresh endpoint, not to every API request. This reduces the attack surface if an attacker finds a way to make requests from the user's browser.
Implement token rotation with a rolling expiration window. Each refresh resets the refresh token's expiration, but cap the total lifetime. For example, refresh tokens expire 7 days from the last refresh, but never more than 30 days from initial issuance. This balances security with user convenience — active users rarely need to log in, but inactive sessions are cleaned up automatically.
Monitor and alert on unusual refresh patterns. Multiple refresh attempts from different IP addresses, refreshes for revoked tokens, and refresh attempts outside normal geographic regions all indicate potential compromise. Log refresh token operations with enough detail to investigate incidents. Use our JWT Decoder during development to inspect token claims and verify your implementation is correct before deploying to production.
Use our free online tool to get started instantly.