JWTs solve one problem well — stateless verification across service boundaries. They've been cargo-culted into session management, a job they're bad at, and the mistake has real security consequences.

Somewhere in a codebase near you, a JWT is being used as a session token. It was probably chosen because "tokens" sounds like "sessions," the tutorial used one, and it worked in development. It keeps working right up until someone needs to force-log-out a compromised account and discovers there's no button that does that. JWTs are a genuinely good tool for one job and a genuinely bad tool for the job they're most often given. The two look similar enough that the mistake is everywhere.

What a JWT actually is

Strip away the mystique and a JWT is a small, signed statement. RFC 7519 defines it in one sentence:

"JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties."

— IETF RFC 7519, "JSON Web Token (JWT)"

Note the words: "representing claims" between "two parties." That's the entire design. Service A signs a set of claims — this user is authenticated, here's their ID, here are their roles — and Service B can verify that signature without calling Service A back. The whole value proposition is stateless verification: B trusts the token because the signature is valid, full stop. That's superb for microservices, OAuth access tokens, and API-to-API trust. Nowhere in that design is there a concept of a session you can end.

What JWTs can't do

A session, by contrast, is stateful by nature: it exists on the server, and the server can destroy it. A JWT can't be destroyed by anyone once it's issued. It's valid until it expires, and the issuer has no way to reach out and cancel it — because the entire point was that verifiers don't have to check back with the issuer. Every "problem" below flows from that single fact.

1. The logout problem

When a user clicks "log out everywhere," a session-based system deletes the session and the job is done. A JWT-based system can't delete the token — it's sitting in the user's browser and copies may be elsewhere. The only way to invalidate it early is to maintain a server-side blocklist of revoked tokens that every verifier checks on every request. Which means you've rebuilt server-side session state, with extra steps and a bigger attack surface, while telling yourself you're stateless. You're not.

2. The payload is readable

JWT payloads are signed, not encrypted. The signature proves the claims weren't tampered with; it does nothing to hide them. Anyone who intercepts the token — or just opens their own browser storage — can read every claim inside: user ID, email, roles, whatever you put there. Decoding it is a one-liner, which you can watch happen by pasting any token into a JWT decoder and reading the middle segment straight out. The lesson: put nothing sensitive in a JWT payload. If you need confidentiality, that's JWE (encrypted), not JWT (signed) — a different tool.

3. The long-expiry trap

Because logout is hard, teams reach for a tempting shortcut: set a long expiry "for user experience," 30 days say, so people don't have to log in often. Now a stolen token is valid for 30 days with no revocation path. You've handed an attacker a month of access and given yourself no way to shut it. The correct pattern is the opposite: short-lived access tokens — 15 to 60 minutes — paired with a refresh token that is stateful and revocable. The access token stays stateless and cheap to verify; the refresh token is where you keep the ability to say no.

4. Signing secrets are real secrets

An HMAC-signed JWT is only as trustworthy as the signing key. Weak key, forged tokens — the whole trust model collapses. The signature is built from a hashing construction (HMAC over the header and payload); you can see the mechanics of the underlying hash with a hash generator. And the signing secret itself should be generated the way you'd generate any high-value credential — long and random, from a password generator, never a memorable string someone picked by hand. A JWT signed with secret123 is decoration, not security.

When JWTs are exactly right

None of this means JWTs are bad. It means they're specialised. Reach for a JWT when:

For a plain web app where users log in, browse, and log out, a boring server-side session is very often the better answer — it revokes instantly, hides its contents by default, and has decades of hardening behind it. The gotcha isn't that JWTs are insecure. It's that "stateless" is a feature until the day you need to reach out and cancel exactly one token — and by then the design decision is already load-bearing.

← All articles