Access Tokens and Refresh Tokens
A short-lived access token proves who you are on every request; a long-lived refresh token quietly mints new ones. Learn why the split exists and how to store each safely.
Prerequisites: JWT (JSON Web Tokens), Authentication vs Authorization
Learning objectives
- Distinguish an access token from a refresh token by lifetime, purpose, and where each is sent.
- Explain why splitting credentials into two tokens limits the damage when one leaks.
- Describe refresh token rotation and how reuse detection revokes a stolen token family.
- Choose an appropriate client-side storage strategy for each token and justify the tradeoffs.
Simple explanation
Think of visiting a theme park. At the gate you show your ID and buy a day pass. From then on you don't hand over your ID at every ride — you just flash the wristband. The wristband is quick to check and, if you lose it, it only works until the park closes tonight. Your ID stays safely in your pocket and is only needed to get a *new* wristband tomorrow.
An access token is the wristband: short-lived and shown constantly. A refresh token is closer to the receipt that lets you get a fresh wristband — it's more powerful, so you keep it locked away and use it rarely.
Technical explanation
After a user logs in, the authorization server issues two tokens:
- Access token — a short-lived credential (typically 5–15 minutes). It is attached to *every* request to a protected resource, usually as
Authorization: Bearer <token>. It is often a signed JWT so the resource server can validate it without a database lookup. - Refresh token — a long-lived credential (days to weeks). It is sent to *one* endpoint only — the token endpoint — for the sole purpose of obtaining a new access token. It is usually an opaque, high-entropy random string tracked in a server-side store.
The access token answers *"is this request allowed right now?"* on the hot path. The refresh token answers *"should this session continue at all?"* on a cold path you control tightly.
Why it matters
The whole reason for two tokens is blast radius. An access token travels on every request, through proxies, logs, and browser memory, so its chance of leaking is comparatively high. By keeping it short-lived, a stolen access token is useless within minutes. The powerful, long-lived refresh token, by contrast, is sent rarely and can be stored more defensively — and because it lives in a server-side store, you can revoke it instantly. Sessions built on a plain long-lived token give you neither property: it leaks widely *and* you can't easily kill it.
How it works
A typical OAuth2 refresh cycle:
- The user authenticates; the auth server returns an access token (short TTL) and a refresh token (long TTL).
- The client calls APIs with
Authorization: Bearer <access-token>. - Eventually the access token expires and the API responds
401 Unauthorized. - The client POSTs the refresh token to the token endpoint:
grant_type=refresh_token. - The server validates the refresh token against its store, then returns a new access token (and, with rotation, a new refresh token, invalidating the old one).
- The client retries the original request with the fresh access token.
Refresh token rotation means every refresh issues a new refresh token and retires the previous one. Reuse detection builds on this: if an already-used (retired) refresh token is presented again, the server assumes it was stolen and revokes the entire token family — forcing every copy, legitimate or not, to re-authenticate.
Real-world analogy
A refresh token is like a hotel key card tied to your reservation at the front desk. The card itself opens your room (issues access), but the front desk can deactivate it the moment you report it lost — that's revocation. Rotation is the hotel quietly re-encoding your card each night: if a thief copied last night's card and tries it after you've been re-issued, the lock rejects it and security is alerted (reuse detection), and your whole stay's cards are invalidated.
Important terminology
- Access token — short-lived credential presented on every resource request; minimize its lifetime.
- Refresh token — long-lived credential exchanged only at the token endpoint for new access tokens; treat it as highly sensitive.
- Rotation — issuing a new refresh token on each refresh and invalidating the old one.
- Reuse detection — revoking the whole token family when a retired refresh token is replayed, a strong signal of theft.
- Token endpoint — the single auth-server endpoint that exchanges credentials or refresh tokens for access tokens.
Key definitions
- Access Token
- A short-lived credential (minutes) sent on every request to access protected resources, commonly a signed JWT.
- Refresh Token
- A long-lived credential (days to weeks) used only against the token endpoint to obtain new access tokens.
- Rotation
- Issuing a new refresh token on every refresh and invalidating the previous one to shrink the window a leaked token is valid.
- Reuse Detection
- Revoking an entire token family when a retired refresh token is presented again — a strong signal it was stolen.
Must memorize
Must memorize
- Access token = short-lived, sent on every request; refresh token = long-lived, sent only to the token endpoint.
- The split exists to limit blast radius: a leaked access token dies in minutes; the refresh token can be revoked centrally.
- Rotation issues a fresh refresh token each time; reuse of an old one revokes the whole token family.
- Prefer an httpOnly, Secure, SameSite cookie for the refresh token; keep the access token in memory, never in localStorage.
Real-world example
Staying logged in on a single-page app
A React SPA keeps the access token in a JavaScript variable and lets the refresh token ride in an httpOnly cookie. When an API call returns 401, an axios interceptor silently POSTs to /auth/refresh, receives a new access token, and retries the request — the user never notices the access token expired every 10 minutes.
Revoking a stolen session after a lost laptop
A user reports a lost laptop. Because refresh tokens live in a server-side store, an admin deletes that user's refresh token rows. The stolen device can finish only the current 10-minute access-token window; its next refresh attempt fails and the session is dead — no need to rotate signing keys or log everyone out.
Code example
# Client obtains tokens at login
POST /auth/login { email, password }
-> 200 { accessToken (exp: 10m), refreshToken (exp: 30d) }
# On each protected request
GET /api/orders
Header: Authorization: Bearer <accessToken>
-> 200 orders # access token still valid
-> 401 Unauthorized # access token expired
# On 401, exchange the refresh token
POST /auth/refresh { refreshToken }
server:
record = store.find(refreshToken)
if record is null OR record.used == true:
# a retired token was replayed -> assume theft
store.revokeFamily(record.familyId)
return 401
record.used = true # rotation: retire old token
newRefresh = issueRefreshToken(familyId = record.familyId)
newAccess = issueAccessToken(userId = record.userId, exp = 10m)
return { accessToken: newAccess, refreshToken: newRefresh }
# Client retries the original request with the new access tokenPseudocode for the token refresh flow with rotation and reuse detection.
import axios from 'axios';
// Access token lives in memory only (not localStorage).
let accessToken = null;
export const setAccessToken = (t) => { accessToken = t; };
const api = axios.create({ baseURL: '/api', withCredentials: true });
api.interceptors.request.use((config) => {
if (accessToken) config.headers.Authorization = 'Bearer ' + accessToken;
return config;
});
// Share one in-flight refresh so parallel 401s don't stampede.
let refreshing = null;
api.interceptors.response.use(
(res) => res,
async (error) => {
const original = error.config;
if (error.response?.status !== 401 || original._retried) {
return Promise.reject(error);
}
original._retried = true;
// The refresh token rides in an httpOnly cookie sent automatically.
refreshing ??= axios
.post('/auth/refresh', {}, { withCredentials: true })
.then((r) => {
setAccessToken(r.data.accessToken);
return r.data.accessToken;
})
.finally(() => { refreshing = null; });
try {
const token = await refreshing;
original.headers.Authorization = 'Bearer ' + token;
return api(original); // retry once with the fresh access token
} catch (e) {
// Refresh failed -> session is over; send the user to login.
window.location.assign('/login');
return Promise.reject(e);
}
},
);
export default api;An axios response interceptor that refreshes on 401 and retries the original request once.
Common mistakes
Mistake
Giving access tokens a long lifetime (hours or days) "so users do not get logged out".
Do this instead
Keep access tokens short (5–15 min) and rely on the refresh token for continuity. A long-lived access token cannot be revoked and stays dangerous the whole time it leaks.
Mistake
Storing tokens in localStorage for convenience.
Do this instead
localStorage is readable by any JavaScript, so an XSS bug leaks the token. Keep the access token in memory and the refresh token in an httpOnly, Secure cookie the JS cannot read.
Interview questions
Quiz
Flashcards
Show answer · Space