The alg:none Trap: How JSON Web Tokens Get Forged — and How to Sign Them Right
JWTs feel almost too clever — until you learn how they get broken. The alg:none bypass, the RS256-to-HS256 key confusion, weak-secret cracking, and the revocation problem nobody warns you about.

A JSON Web Token is one of those rare pieces of technology that feels almost too clever. You take some data, sign it, and hand it to the user. From then on, the user carries their own identity card — and your server can check it's genuine without looking anything up, without a database, without remembering a thing. It's elegant, it scales beautifully, and it powers a huge slice of modern logins.
It's also been the source of some of the most instructive security failures of the last decade. The very features that make JWTs elegant are the same ones that, handled carelessly, let attackers walk straight through the front door holding a forged badge. The history of how that happened is genuinely fascinating — and understanding it changes how you create tokens forever.
What a token actually is
Before the failures, the shape. A JWT is three chunks of base64url text joined by dots: header.payload.signature. The header says which algorithm signed the token. The payload holds the claims — who the user is, when the token expires, and so on. The signature is the clever bit: a cryptographic seal computed over the header and payload using a secret key.
Crucially, the first two parts are merely encoded, not encrypted. Anyone can read them. Paste any token into a decoder and the claims fall right out. The signature isn't there to hide the data — it's there to prove the data hasn't been changed. Flip a single character in the payload and the signature no longer matches, so the server rejects it. That's the whole security model: not secrecy, but tamper-evidence. When you build a token in a JWT generator, the colour-coded segments make this obvious — you can see your claims sitting in plain sight, with the signature as the only part that depends on the secret.
So the entire safety of a JWT rests on one question: can an attacker produce a valid signature? The answer was supposed to be a firm no. For a few embarrassing years, it was often "yes."
The most famous flaw: alg: none
Here's a detail almost nobody expects. The JWT standards deliberately allow a token with no signature at all. It's called an "unsecured" JWT, and you mark it by setting the header algorithm to the literal string none and leaving the signature section empty. It exists for niche cases where the token travels over an already-trusted channel.
You can probably see the disaster coming. In 2015, the security researcher Tim McLean published a now-legendary analysis of JSON Web Token libraries and found that many of them handled alg: none catastrophically. The verification code would read the algorithm from the token itself, see none, and conclude: "Ah, this token doesn't need a signature — so it's valid."
That's like a bouncer checking the guest list, but letting you write your own name on a slip of paper that says "the bouncer doesn't need to check this one." An attacker could take any token, change the header to {"alg":"none"}, rewrite the payload to claim they were the administrator, delete the signature entirely, and send it. A vulnerable server accepted it without a second thought. No key, no cracking, no cleverness — just asking the system to please not check.
The fix is almost philosophical: never let the attacker's token tell you how to verify the attacker's token. The server must decide the algorithm in advance and refuse anything else. But the bug kept reappearing for years because the trusting behaviour felt so natural to implement.
The sneakier one: confusing the keys
The second classic attack is subtler and, in a way, more beautiful. It exploits the gap between two families of signing algorithms.
Algorithms like HS256 are symmetric: the same secret both creates and verifies the signature. Algorithms like RS256 are asymmetric: a private key signs, and a separate public key verifies. The public key is, by design, public — you're meant to share it freely, because knowing it doesn't let you forge anything.
Now imagine a server set up for RS256. It holds a public key to verify incoming tokens. A naive verification routine again trusts the algorithm written in the token's header. So the attacker switches the header from RS256 to HS256, then signs their forged token using HMAC — with the server's public key as the HMAC secret. When the server receives it, sees "HS256," and dutifully runs an HMAC check using the only key it has (the public one), the math lines up. The forged token verifies.
The attacker turned a key everyone was allowed to know into the key that unlocked everything. The root cause is identical to the alg: none problem: the code trusted the token to describe how it should be checked. Pin the algorithm to exactly what you expect, and both attacks evaporate.
When the secret is just too weak
Not every break is a clever trick. Many are brute force.
With a symmetric token like HS256, security rests entirely on the secret being unguessable. But remember — the whole token is public, and so is the signing input. That means an attacker can take a token they captured, then sit offline and try millions of candidate secrets per second, recomputing the signature for each until one matches. There's no server to rate-limit them, no lockout, no alarm. If your secret is secret, password, your company name, or anything in a wordlist, a tool like a GPU password cracker will find it in moments.
This is why a JWT secret isn't a password you should be able to remember. For HS256 it ought to be at least 256 random bits; HS384 and HS512 want more still. A long string of pure randomness is the goal, which is exactly why a good generator will measure your secret's length against the algorithm you picked and warn you when it falls short. That little check has saved more breaches than any amount of advice, because "use a strong secret" is easy to nod at and easy to ignore until something flags it in front of you. If you want to feel it, generate a token with a flimsy secret in the JWT generator and watch the strength indicator turn from a reassuring tick to a warning.
The problem nobody warns you about: you can't take it back
Even a perfectly signed, strongly-secured token has one inherent quirk that trips up almost every team eventually. It comes straight from the feature everyone loves: statelessness.
Because a JWT is self-contained and the server keeps no record of it, the server has no natural way to cancel one. A traditional session lives in a database; to log someone out, you delete the row, and the session is dead instantly. A JWT lives only in the user's hands. Once you've signed and issued it, it is valid until it expires — full stop. If a token is stolen, or a user clicks "log out everywhere," or an account is banned, the token keeps working until its expiry time arrives.
Teams discover this the hard way and then bolt on workarounds: a blocklist of revoked tokens (which quietly reintroduces the database lookup JWTs were meant to avoid), or very short expiry times paired with refresh tokens. The standard mitigation is the humble exp claim — keep access tokens short-lived, often just minutes, so a leaked one is only briefly dangerous. It's why thoughtfully setting an expiry isn't an afterthought; it's the main lever you have over a token you can never recall. Setting exp with a sensible window the moment you create a token is the single most effective habit, and it's why one-tap expiry presets exist at all.
This trade-off has fuelled a long-running debate among engineers: JWTs are superb for short-lived, cross-service authorisation, but often a poor fit for ordinary website sessions, where a plain old session cookie is simpler, instantly revocable, and harder to misuse. The technology isn't the problem; reaching for it in the wrong place is.
Signing them right
Strip away the war stories and a short, durable checklist remains. Decide the algorithm on the server and reject anything else, so no token can talk you into a weaker check. Treat the secret as the crown jewel it is — long, random, never committed to a repository, never shared across environments. Always set an expiry, and keep it short. Remember that the payload is readable by anyone, so never tuck a password or secret inside it. And use JWTs for what they're brilliant at — passing verifiable claims between services — rather than as a clever replacement for things that were working fine.
The deeper lesson is almost human. Every one of these famous failures came not from broken cryptography but from a system being a little too trusting — believing the token about how it should be judged, or believing a weak secret was good enough because nothing ever pushed back. Good security, like good signing, is mostly the discipline of not trusting the things that ask to be trusted. Build a few tokens yourself, watch the pieces light up, change the algorithm and the secret and see what the tool says — and the abstract suddenly becomes something you can reason about with confidence.