Map

JWT for Sessions (Anti-Pattern)

Wiki conceptauthenticationsecurityweb โ†ณ show in map Markdown
title
JWT for Sessions (Anti-Pattern)
type
concept
summary
Why using JWTs as session tokens is the wrong tool โ€” lifetime mismatch, false statelessness, spec footguns, and what to use instead
tags
authentication, security, web
created
2026-04-25
updated
2026-04-25

A pattern, popular in JavaScript bootcamp curricula and tutorial-driven learning, of using JSON Web Tokens (JWT, RFC 7519) as the user-session credential. After login, the server returns a JWT containing the user's identity claims; the client stores it (often in localStorage) and sends it on each request, usually as Authorization: Bearer <token>. The server validates the JWT signature on each request without looking anything up. This is sold as "stateless authentication."

Several long-running write-ups (joepie91 2016, Paragon Initiative 2017, samsch gist) make a converging case that this is the wrong tool. The standard alternative is regular cookie sessions.

Why it doesn't fit

Lifetime mismatch. JWTs are designed for very short-lived tokens (~5 minutes or less). Sessions need longer lifespans โ€” hours, days, sometimes weeks. Stretching the lifetime turns every flaw of the design into a liability that grows with time on the wire.

Revocation is a hard add-on. A signature-only validation pipeline has no way to invalidate a token before its exp. Real systems need to log users out, kick a compromised account, or roll an admin's permissions in real time. Bolting revocation onto JWTs means a server-side blocklist that has to be checked on every request โ€” at which point the "stateless" property is gone and the JWT is no longer doing anything a session ID couldn't do better.

Statelessness is a lie at any reasonable security level. Real auth needs revocation, key rotation, replay protection, and a way to invalidate sessions on password change. All of these need server state. The argument from samsch (and joepie91) is that once you accept you need a data store, store everything in it and just hand out an opaque session ID. The JWT only buys you payload-on-the-wire, which costs more bytes and gains nothing.

The JWT/JOSE spec family has a poor security track record. The original spec allowed alg: none (an unsigned JWT that says "trust me"); some libraries shipped this as the default. Multiple critique-pieces detail confusion between symmetric/asymmetric algorithms, key-confusion attacks, and footguns in the broader JOSE family. Paragonie's "JWT is a Bad Standard" is the canonical takedown.

Bigger and slower. A JWT carrying only a session-ID claim is larger than a session cookie, must be parsed and signature-verified on every request, and gains nothing.

Why people reach for it anyway

  • It's the default in many JavaScript and Node.js tutorials.
  • "Stateless" sounds appealing for horizontal scaling โ€” until you realize you need a Redis/Postgres for revocation either way.
  • Mobile and SPA developers were told cookies are old-fashioned and Bearer tokens are modern. They aren't; cookies (specifically HttpOnly; Secure; SameSite=Strict|Lax) are the right tool for browser auth and work perfectly well for the SPA case.
  • Microservice architectures where one service authenticates and others verify without a shared session store. JWT works here, but this is the between-services case, not the user-session case.

Where JWTs (or paseto) actually fit

The same critics agree there are legitimate uses for signed short-lived tokens, all of them at the ~5-minute end of the lifetime spectrum:

  • Single-sign-on transports (the Google pattern) โ€” log in at host A, hand a JWT to host B as the once-used proof, host B mints its own session.
  • Password-reset and email-verification links.
  • Capability tokens (signed download URLs, signed action tokens).
  • Service-to-service auth in microservices.

Even for these, paseto is now considered a better-designed alternative.

Don't pair the anti-pattern with localStorage

Storing JWTs in localStorage or sessionStorage makes any XSS into a credential exfiltration. Browser JS can read every entry. HttpOnly cookies cannot be touched by JS at all and survive XSS. The 2018 Randall Degges post linked from the samsch gist is the standard cite.

  • cookie-session-auth โ€” the actual recommended pattern for browser sessions
  • paseto โ€” the better-designed short-lived-token spec for the cases where you do need one
  • samsch-jwt-auth-gist โ€” the link-friendly summary version of this case