1Step-by-Step Breakdown
The Core Tradeoff a Single Long-Lived Token Forces. A single JWT with a long expiration (say, 30 days) minimizes login friction but maximizes damage if stolen — it remains valid for the attacker for the full 30 days. A short expiration (5 minutes) minimizes stolen-token damage but forces the user to re-login constantly. Refresh tokens resolve this tradeoff.
The Two-Token Model. A short-lived "access token" (minutes) authorizes actual API requests, while a longer-lived "refresh token" (days/weeks) has exactly one purpose: exchanging itself for a new access token when the current one expires — the access token's short life limits stolen-token exposure; the refresh token minimizes how often the user re-authenticates.
Implementing the Refresh Endpoint. A dedicated /auth/refresh endpoint accepts a valid refresh token and issues a brand-new access token (and often a new refresh token too, covered in Token Rotation) — this endpoint is the ONLY place a refresh token is ever used, never sent alongside regular API requests.
Storing Refresh Tokens Server-Side for Revocation. Unlike a stateless access token, a refresh token's validity should be checkable and revocable server-side (stored in a database, associated with the specific user and device) — this is what makes "log out this device" or "revoke all sessions after a suspected compromise" actually possible, which a purely stateless token cannot support.
Where Each Token Should Live in the Client. An access token can reasonably live in memory (JavaScript variable, lost on page refresh, refetched via the refresh token) since its short life limits exposure risk. A refresh token, being longer-lived and higher-value, belongs in an HttpOnly cookie (covered in Secure Cookies) — never in localStorage, which is readable by any XSS payload.
Handling Refresh Failure Gracefully on the Client. A client-side HTTP interceptor that automatically attempts a token refresh when an API call returns 401 (access token expired), retrying the original request with the new token, provides a seamless experience — the user never notices their access token expired, as long as their refresh token remains valid.
The Refresh Token's Own Expiration and Re-Login. Even a refresh token eventually expires — at that point, the user genuinely must re-authenticate with credentials, which is the deliberate, acceptable tradeoff: a refresh token's expiration (days/weeks) balances security (bounding the absolute maximum session length) against convenience (far less frequent than the access token's own expiration).
Why should a refresh token be stored in an HttpOnly cookie rather than localStorage, given that an access token is often kept in memory instead?
- →A refresh token is longer-lived and higher-value, and localStorage is readable by any XSS payload — HttpOnly blocks that access
- →localStorage has a size limit too small to store a refresh token
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Seamless Token Refresh Prevents Unexpected Session Interruptions Mid-Task
A transparent client-side refresh mechanism prevents a user from being unexpectedly logged out mid-task simply because their access token expired — an interruption that would be especially disruptive for a user relying on assistive technology who may have invested significant extra time completing a long, multi-step form.
SEO Implications
- 1
Revocable Refresh Tokens Enable Fast Incident Response to a Compromised Account
Server-side revocable refresh tokens allow immediate session termination in response to a suspected account compromise or security incident, meaningfully reducing the duration and impact of the incident, which directly affects the severity of any resulting trust and reliability damage.
Best Practices
Use a short-lived access token for regular API requests, paired with a longer-lived, server-side revocable refresh token
This resolves the tradeoff a single token forces between minimizing stolen-token exposure and minimizing login friction, giving you both properties simultaneously.
Store the refresh token in an HttpOnly, Secure cookie, never in localStorage
The refresh token is the higher-value, longer-lived credential — localStorage is readable by any XSS payload, while HttpOnly blocks JavaScript access entirely.
Frequent Bugs
After a user reports their account was compromised, the security team has no way to immediately terminate that user's active sessions across all their devices.
This points to refresh tokens being issued as purely stateless tokens with no server-side record, making revocation impossible before natural expiration. Persist issued refresh tokens server-side (associated with the user and device) so they can be explicitly revoked in response to an incident.
Real-World Examples
Enabling Fast Incident Response Through Revocable Refresh Tokens
A user reported their laptop was stolen while still logged into the company's application. Because refresh tokens were stored server-side per device, the support team was able to immediately revoke that specific device's refresh token, forcing it to fail on its next refresh attempt and requiring full re-authentication — while the user's other, trusted devices remained unaffected. Without server-side revocation, the stolen laptop's session would have remained valid until the refresh token's natural 7-day expiration, a significantly longer exposure window.
// The targeted, per-device revocation that resolved the incident
await db.refreshTokens.delete({ userId, deviceId: stolenLaptopDeviceId });