Back to Security
Security
medium
mid

How would you implement token based authentication (such as 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.

5 min read·~10 min to think through

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

ts
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 /login

Usually 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.

Senior engineer discussion

Seniors design the two-token model, default to httpOnly cookies with CSRF protection, implement refresh via an interceptor, solve the concurrent-refresh race with a shared promise, and cover rotation, server-side logout, multi-tab sync, and that the server is the real validator.

Related questions