Overview
Use an opaque session token in an HTTP-only cookie for browser user sessions. A JWT can carry a session, but a long-lived JWT stored in localStorage and refreshed on the client gives up revocation and exposes the token to JavaScript. Keep JWTs for short-lived, service-level claims, as described in oauth-vs-jwt. The cookie and rotation rules themselves live in auth-sessions.
When session cookies win
Choose server-side sessions for any user logged in through a browser.
- Revocation: deleting the session row logs the user out everywhere on the next request. Logout, password change, and “sign out all devices” work without extra machinery.
- XSS exposure: an
HttpOnlycookie is not readable from JavaScript, so an injected script cannot copy the token out. It can still make requests as the user while the page is open, so XSS remains serious, but the credential does not leave the browser. - Small payload: the cookie carries a random ID, not a bundle of claims, and the server looks up current roles on each request, so permission changes apply immediately.
When JWTs win
Choose a JWT when the verifier cannot or should not look up state.
- Service-to-service calls inside a trust boundary, where the receiver validates a signature instead of querying a session store.
- Short-lived access tokens issued by an OAuth authorization server.
- Claims propagation through a gateway to downstream services.
In each case the token lifetime is short, so the lack of revocation matters less.
The JWT-as-session antipattern
Storing a long-lived JWT in localStorage and refreshing it on the client is a common antipattern. It trades the security of HTTP-only cookies and server-side revocation for a stateless token that cannot be invalidated if compromised.
localStorageis accessible to JavaScript; an XSS vulnerability exfiltrates the token.- A 30-day JWT cannot be revoked without a blocklist, which negates the statelessness benefit.
- The correct pattern for user sessions: issue an opaque session token, store it in an HTTP-only
Securecookie, keep session state server-side in Redis with a short TTL and sliding expiry. - JWTs are for service-level claims transport, not user session persistence.
Trade-offs at a glance
| Dimension | Opaque session cookie | Long-lived JWT in localStorage |
|---|---|---|
| Revocation | Delete the server-side record | Blocklist or wait for expiry |
| Readable by JS | No, with HttpOnly | Yes |
| Server lookup | One store read per request | None |
| Permission changes | Apply on next request | Apply at next token refresh |
| CSRF exposure | Needs SameSite and CSRF defenses | None from cookies; XSS is the risk |
Migrate JWT sessions to opaque sessions
Introduce a session store such as Redis (see redis-vs-memcached), generate opaque tokens, set them as HTTP-only cookies, and add session middleware that loads the session on each request. Client changes are 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, as covered in auth-sessions.