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
- After successful authentication, generate a session token with at least 128 bits of randomness from a secure generator.
- Store only a hash of the token server-side, with user ID, authentication level, methods used, auth time, and device label (Chapter 3).
- Issue a new token on every privilege change: sign-in, step-up, and sign-out of other sessions. This prevents session fixation.
- 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: noneand any algorithm you did not configure. - Select the key by
kidfrom your own JWKS, never from a URL inside the token. - Validate
iss,aud,exp,nbf, andiatwith a small clock-skew allowance. - Keep tokens small. Put user attributes behind
/userinfoor 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.
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.