Auth for Web and Mobile: Access + Refresh Token Best Practices
I recently revisited one of my auth projects and asked myself a fair question: is my access + refresh token flow truly production-ready?
This post is my practical reference for how I now design authentication for React/Next.js web apps and mobile apps.
Why use access token + refresh token?
- Access token: short-lived token used on API requests (for example, 5-15 minutes).
- Refresh token: longer-lived token used only to get a new access token.
This gives a strong balance:
- Short lifetime limits blast radius if an access token leaks.
- Refresh flow keeps users logged in without constant re-login.
Recommended storage strategy
Web (React or Next.js SPA behavior)
- Store access token in memory only (React state/store/module variable).
- Store refresh token in HttpOnly, Secure cookie.
- Set
SameSite=Lax(orStrictwhen possible), and narrowPath(for example/api/auth/refresh). - Do not put refresh tokens in
localStorageor regular JS-readable cookies.
Why: if XSS happens, in-memory access tokens are short-lived, while HttpOnly cookies protect refresh tokens from direct JavaScript reads.
Mobile (iOS/Android)
- Store access token in memory.
- Store refresh token in secure OS storage:
- iOS Keychain
- Android Keystore / encrypted storage
- Never store long-lived tokens in plain async/local storage.
The flow I recommend
- User logs in with credentials.
- Server returns:
- Access token in response body.
- Refresh token via secure cookie (web) or response body for secure storage (mobile).
- Client sends access token in
Authorization: Bearer <token>. - On access token expiry (
401), client calls refresh endpoint once. - Server validates refresh token, rotates it, and returns new access token.
- Client retries the original request.
- On logout, revoke refresh token server-side and clear cookie/secure storage.
Critical backend hardening rules
- Keep access token TTL short (5-15 minutes).
- Rotate refresh token on every refresh.
- Store refresh tokens hashed in DB (not plaintext).
- Track token metadata:
userId,sessionId/deviceId,createdAt,expiresAt,revokedAt. - Detect refresh token reuse (possible theft) and revoke the whole session family.
- Add rate limiting on login and refresh endpoints.
- Validate audience/issuer/signing keys consistently.
- Use HTTPS everywhere in production.
CSRF and CORS notes for cookie-based refresh on web
If refresh token is in cookie, your refresh endpoint can be CSRF-sensitive. Typical protections:
SameSite=LaxorStrictfor refresh cookie.- Anti-CSRF token for sensitive state-changing requests.
- Tight CORS allowlist with
credentials: trueonly for trusted origins.
Frontend implementation tips (React/Next.js)
- Use an API client interceptor, but prevent refresh stampede:
- If 10 requests fail with
401, trigger one refresh request. - Queue/retry the other 9 after refresh succeeds.
- If 10 requests fail with
- Add retry guard to avoid infinite refresh loops.
- If refresh fails, clear auth state and redirect to login.
- In Next.js, do not leak secrets to client bundles; keep auth-sensitive logic on server routes where possible.
Common mistakes I see often
- Refresh token stored in
localStorage. - No refresh token rotation.
- Old refresh token still valid after issuing a new one.
- Missing logout revocation on server.
- Broad cookie scope (
Path=/) when only refresh endpoint needs it. - Infinite
401 -> refresh -> retryloops on the client.
My practical default in 2026
For web:
- Access token in memory.
- Refresh token in
HttpOnly + Secure + SameSitecookie. - Rotation + reuse detection on backend.
For mobile:
- Access token in memory.
- Refresh token in Keychain/Keystore.
- Same backend rotation and revocation model.
This model is not the only model, but it is a reliable and secure baseline I can apply across most product teams.