Skip to content

Chapter 6 of 205 min readBy Deepak GuptaPublished

Sessions and Tokens

Use opaque, server-side sessions in hardened cookies for your own web apps. Use short-lived JWT access tokens plus rotating refresh tokens only where a token has to cross a trust boundary: mobile apps, APIs, and third-party clients.

Most session bugs come from mixing these two models. A JWT stored in localStorage for a first-party web app gives you the downsides of both: it can be stolen by any script on the page, and you cannot revoke it.

Browser sessions, step by step

  1. After successful authentication, generate a session token with at least 128 bits of randomness from a secure generator.
  2. Store only a hash of the token server-side, with user ID, authentication level, methods used, auth time, and device label (Chapter 3).
  3. Issue a new token on every privilege change: sign-in, step-up, and sign-out of other sessions. This prevents session fixation.
  4. Send it in a cookie with every protective attribute:
Set-Cookie: __Host-sid=<token>; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=2592000
Attribute Why
__Host- prefix Browser enforces Secure, Path=/, and no Domain, so subdomains cannot set or overwrite it
Secure Never sent over plain HTTP
HttpOnly Not readable by JavaScript, so cross-site scripting cannot steal it directly
SameSite=Lax Not sent on most cross-site requests, which blocks most CSRF; still add CSRF tokens for state-changing forms
Max-Age Absolute lifetime on the device; enforce the real timeout server-side too

Timeouts

Keep two clocks on every session: an idle timeout and an absolute lifetime. At AAL2, NIST SP 800-63B-4 requires an overall reauthentication timeout and recommends it be no more than 24 hours, with an inactivity timeout of no more than 1 hour. (NIST) Consumer products usually keep long sessions on trusted devices and require fresh authentication only for sensitive actions. Store auth_time so you can ask for step-up (Chapter 7) without ending the session.

Sign-out and revocation

  • Sign out deletes the server-side session, clears the cookie, and revokes refresh tokens for that device.
  • Sign out everywhere revokes every session and refresh token for the user. Trigger it automatically on password change, recovery, and factor removal.
  • Session list shows users their active devices with a revoke button. It doubles as an account-takeover detector.

Access tokens and refresh tokens

When tokens must cross a boundary:

Token Lifetime (typical) Format Storage
Access token 5 to 15 minutes Signed JWT or opaque with introspection Memory only on clients
Refresh token Days to weeks, with an idle limit Opaque, stored hashed server-side Secure OS storage on mobile; never in browser JavaScript
ID token (OIDC) Minutes Signed JWT Consumed once by the client to learn who signed in

RFC 9700, the OAuth 2.0 Security Best Current Practice, requires refresh tokens for public clients to be either sender-constrained or rotated. Rotation means every use returns a new refresh token and invalidates the old one. If an old one is ever presented again, assume theft and revoke the whole token family. (RFC 9700)

RFC 9700 also recommends sender-constraining access tokens with mutual TLS or DPoP (RFC 9449), so a stolen token is useless without the client's private key. Start with rotation; add DPoP for high-value APIs.

Single-page apps

Use the backend-for-frontend pattern: the SPA talks to its own backend with a session cookie, and the backend holds OAuth tokens. Tokens never reach browser JavaScript.

JWT rules

  • Pin allowed algorithms per key. Reject alg: none and any algorithm you did not configure.
  • Select the key by kid from your own JWKS, never from a URL inside the token.
  • Validate iss, aud, exp, nbf, and iat with a small clock-skew allowance.
  • Keep tokens small. Put user attributes behind /userinfo or your API, not in every token.
  • Never put secrets or personal data you would not want logged into a JWT payload. It is signed, not encrypted.
Warning

Common mistakes

  • Tokens in localStorage.
  • Long-lived JWT access tokens with no revocation path.
  • Not rotating the session ID after sign-in.
  • Sign-out that clears the cookie but leaves the server-side session valid.

Key takeaways

  • Opaque server-side sessions in __Host- cookies for first-party web apps; tokens only across trust boundaries.
  • Rotate the session ID on every privilege change to stop fixation.
  • Rotate refresh tokens and revoke the whole family on reuse, as RFC 9700 requires for public clients.
  • Pin JWT algorithms, pick keys by kid from your own JWKS, and validate iss, aud, and expiry.