JWT in 2026: When It's Right, When It's a Footgun
JWTs are not a session system. They are a signed claim. Confusing the two is how teams ship subtle auth bugs.
Every few months I review a service whose auth design is "JWT in localStorage, valid for a week, no revocation." It mostly works. Until it doesn't, and the postmortem is the same one every time. JWTs are not the problem — using them as a session store is.
1. What a JWT actually is
Three base64url segments joined by dots: header, payload, signature. The header says how the signature was computed; the payload is a JSON blob of claims; the signature is the proof that the issuer signed it.
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyXzQyIiwiZXhwIjoxNzM4...
<rsa signature bytes>
The signature lets you verify the payload was issued by the holder of the key. That is all it does.
2. The fundamental property: you cannot un-issue one
Once a JWT is signed and handed to a client, every server that knows the key will accept it until exp passes. There is no central session store to delete from. That is the feature — verification needs only the public key, not a database — and the trap.
If you need to log a user out, ban an account, or rotate compromised credentials, you cannot, unless you also maintain a revocation list. Which is a session store. Which is what you were trying to avoid.
3. The four wrong shapes
3.1 Long-lived JWT as a session
The classic. A 7-day token in localStorage, no refresh, no revocation. Steal the token → impersonate the user for a week. Don't do this.
3.2 JWT signed with HS256 + shared secret across services
Symmetric signing means every verifier also has the keys to sign. One compromised service issues tokens that all the others accept. Use RS256 / EdDSA where the signer holds a private key and verifiers hold only the public one.
3.3 The none algorithm
The JWT spec accepts alg: none as "no signature." Some libraries used to honour this on verify. Today most don't, but pin the expected algorithm explicitly in your verifier — never let the token tell you how to verify itself.
3.4 Trusting the kid header without validation
The kid (key ID) header tells you which key to verify with. Many libs lookup by it. If the value is unvalidated, an attacker points it at an arbitrary key — sometimes a URL, sometimes a path on disk. Whitelist allowed key IDs.
4. The shape that actually works
- Short-lived access token — 5–15 minutes. JWT, used by every service.
- Long-lived refresh token — opaque (NOT a JWT), stored server-side, revocable. Used only at
/auth/refresh. - Refresh rotation — every refresh issues a new refresh token and invalidates the old one. Detect re-use as a theft signal.
- HttpOnly cookies for the refresh token. JS cannot read it; XSS can't exfiltrate it.
The access token can be in memory or in a non-HttpOnly cookie. The refresh token must not be reachable by client JavaScript.
5. Claims you should always put in the payload
iss— who issued the token. Verifiers MUST check.aud— who the token is for. Prevents a token for service A being replayed at service B.exp— expiry. Short.iat— issued-at. Lets you reject any token issued before a "all sessions invalid before X" timestamp per user.jti— JWT ID. Required if you want to deny-list individual tokens.
6. When NOT to use JWT
- First-party web apps with a session backend — use server-side opaque sessions. They are simpler, revocable, and don't leak claims.
- Anywhere "logout" needs to be immediate.
- Anywhere the token would be exposed to JavaScript in localStorage.
Rules of thumb
- JWT is for service-to-service trust and short-lived API access — not for session management.
- Always pair short-lived JWT with a longer-lived, server-revocable refresh.
- Always pin algorithms and validate
iss+audexplicitly.