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 (or Strict when possible), and narrow Path (for example /api/auth/refresh).
  • Do not put refresh tokens in localStorage or 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

  1. User logs in with credentials.
  2. Server returns:
    • Access token in response body.
    • Refresh token via secure cookie (web) or response body for secure storage (mobile).
  3. Client sends access token in Authorization: Bearer <token>.
  4. On access token expiry (401), client calls refresh endpoint once.
  5. Server validates refresh token, rotates it, and returns new access token.
  6. Client retries the original request.
  7. 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=Lax or Strict for refresh cookie.
  • Anti-CSRF token for sensitive state-changing requests.
  • Tight CORS allowlist with credentials: true only 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.
  • 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 -> retry loops on the client.

My practical default in 2026

For web:

  • Access token in memory.
  • Refresh token in HttpOnly + Secure + SameSite cookie.
  • 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.