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 HttpOnly cookie 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.

  • localStorage is 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 Secure cookie, 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

DimensionOpaque session cookieLong-lived JWT in localStorage
RevocationDelete the server-side recordBlocklist or wait for expiry
Readable by JSNo, with HttpOnlyYes
Server lookupOne store read per requestNone
Permission changesApply on next requestApply at next token refresh
CSRF exposureNeeds SameSite and CSRF defensesNone 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.