How would you implement token-based authentication (e.g., JWT) and handle token expiry
Short-lived access token + long-lived refresh token. Store them safely (httpOnly cookies preferred over localStorage). On a 401, use the refresh token to get a new access token transparently, queueing in-flight requests; if refresh fails, log out. Never trust the client — the server validates the token.
JWT auth is a two-token system plus a transparent refresh flow. The implementation details — storage and the refresh race — are what's being tested.
The two-token model
- Access token — short-lived (5–15 min), sent with every request, proves who you are. Short lifetime limits the damage if it leaks.
- Refresh token — long-lived (days/weeks), used only to obtain a new access token when it expires. Stored more carefully; can be rotated and revoked server-side.
Storage — the key security decision
- httpOnly, Secure, SameSite cookies (preferred) — JS can't read them, so XSS can't steal the token. The browser sends them automatically. Pair with CSRF protection (SameSite=strict/lax + CSRF token).
- localStorage — readable by any script, so any XSS = token theft. Avoid for tokens. If you must use a header-based scheme, keep the access token in memory (a JS variable / context) and the refresh token in an httpOnly cookie.
The refresh flow
request → 401 (access token expired)
→ call /refresh with the refresh token
→ got a new access token → retry the original request
→ refresh also failed → log out, redirect to /loginUsually implemented as an HTTP interceptor (axios interceptor / a fetch wrapper) so it's transparent to the app code.
The detail that separates seniors: the refresh race
If 5 requests fire and all get 401 at once, you must not kick off 5 refresh calls. Pattern:
- The first 401 starts the refresh and stores the in-flight refresh promise.
- Concurrent 401s await that same promise instead of starting their own.
- When it resolves, all queued requests retry with the new token.
- If refresh fails, reject them all and log out.
Other concerns
- Refresh token rotation — issue a new refresh token on each use; detect reuse of an old one as theft and revoke the session.
- Logout — clear tokens client-side and invalidate the refresh token server-side (JWTs are otherwise valid until expiry).
- Don't trust the client — the server validates the JWT signature and expiry on every request. Never decode-and-trust on the client beyond reading non-sensitive claims for UI.
- Multi-tab — sync logout/refresh across tabs (storage event / BroadcastChannel).
- Clock skew & proactive refresh — optionally refresh just before expiry instead of waiting for the 401.
The framing
"Two tokens: a short-lived access token sent on every request, and a long-lived refresh token used only to mint new access tokens. Storage is the key call — httpOnly Secure cookies so XSS can't read them, with CSRF protection; never localStorage for tokens. Expiry is handled with an interceptor: on a 401, refresh and retry transparently; if refresh fails, log out. The senior detail is the refresh race — concurrent 401s must await one shared refresh promise, not trigger a stampede — plus refresh-token rotation and server-side logout. And the server validates every token; the client never trusts itself."
Follow-up questions
- •Why httpOnly cookies over localStorage for tokens?
- •How do you prevent a refresh-token stampede on concurrent 401s?
- •What is refresh token rotation and what does it protect against?
- •How do you actually log a JWT user out, given JWTs are stateless?
Common mistakes
- •Storing the access (and refresh) token in localStorage — XSS-readable.
- •Firing multiple concurrent refresh requests on a burst of 401s.
- •No refresh flow — sessions break abruptly at expiry.
- •Assuming logout works just by deleting the client token (refresh token still valid server-side).
- •Decoding the JWT on the client and trusting it for authorization.
Performance considerations
- •A naive flow causes a refresh stampede and request waterfalls — queue concurrent requests behind one refresh promise. Proactive refresh before expiry avoids the 401-retry round-trip on the critical path.
Edge cases
- •Multiple requests getting 401 simultaneously.
- •Refresh token itself expired or revoked.
- •User logged in across multiple tabs/devices.
- •Clock skew between client and server.
Real-world examples
- •Axios response interceptors implementing transparent token refresh.
- •Auth providers (Auth0, Clerk) handling rotation and refresh out of the box.