Overview
Use an opaque session ID in an HttpOnly, Secure cookie for browser users, with the session stored server-side. A long-lived JWT kept in localStorage cannot be revoked before expiry and is readable by any script that runs on the page. Keep JWTs for short-lived claims between services, as described in oauth-vs-jwt. Cookie flags, rotation, and CSRF rules live in auth-sessions.
Comparison
| Dimension | Opaque session cookie | Long-lived JWT in localStorage |
|---|---|---|
| Revocation | Delete the server record; takes effect on the next request | Blocklist (which adds state) or wait for expiry |
| Readable by page JavaScript | No, with HttpOnly | Yes |
| Server work per request | One session lookup | Signature verification |
| Permission changes | Apply on the next request | Apply at the next refresh |
| CSRF | Needs SameSite and CSRF defenses | Not from cookies; XSS is the main risk |
| Stateless verification | No | Yes |
Pick session cookies when
Pick server-side sessions for any user logged in through a browser.
- Logout, password change, and “sign out everywhere” must work immediately.
HttpOnlystops a script from copying the credential, though XSS can still make authenticated requests while the page is open.- The cookie holds a random ID; roles and permissions are read fresh from the store.
- Use
SameSite=LaxorStrict, rotate the ID on login, set sliding and absolute expiry, and add CSRF tokens for state-changing requests.
Pick JWTs when
Pick JWTs when the verifier should not look up state.
- Service-to-service calls inside a trust boundary, and claims forwarded by a gateway.
- Short-lived access tokens from an OAuth authorization server (RFC 9068 defines a JWT profile for them).
- Keep lifetimes to minutes so missing revocation matters less; validate signature,
exp,aud, andiss, and pin the accepted algorithm.
Migrate
Introduce a session store (Redis; see redis-vs-memcached), issue random IDs as HttpOnly cookies, and add middleware that loads the session on each request. The change is small if the JWT already lived in a cookie and larger if client code read it from localStorage. Add CSRF protection when moving to cookies.