All posts
Ethical Hacking

Breaking and Securing JWT Authentication: Real-World Token Attacks

alg=none, algorithm-confusion, weak secrets, and token replay — the JWT attacks every backend engineer should understand, and how to defend.

SKSushan Khadka
April 8, 2026 (4mo ago)6 min read
Breaking and Securing JWT Authentication: Real-World Token Attacks — article by Sushan Khadka (namelessnerd)

A JWT is just a signed claim about who you are. If the verifier trusts that signature blindly, your entire login system rests on one fragile assumption — and attackers know exactly where to push.

Breaking and Securing JWT Authentication: Real-World Token Attacks - diagram by Sushan Khadka (namelessnerd)

The anatomy

A token is three base64url chunks: header.payload.sig. The header names the algorithm, the payload carries claims like sub, exp, and aud, and the signature proves the server minted it. Everything before that signature is readable by anyone — base64 is not encryption. The only thing between a user and forged admin claims is whether the server verifies that last segment correctly.

Decode a real token and you will see something like this:

// header
{ "alg": "HS256", "typ": "JWT" }
// payload
{ "sub": "1024", "role": "user", "exp": 1735689600, "iss": "auth.myapp.com" }

Paste any JWT into a decoder and both halves render instantly, no key required. That is by design — the payload is meant to be transparent. The mistake engineers make is treating it as tamper-proof. It is not tamper-proof; it is tamper-evident, and only if you check the evidence.

The big three bugs

These are the failures that show up again and again in real audits:

Walking the alg-confusion attack

This one deserves a slower look because it is the most misunderstood. In RS256, the server holds a private key and signs; anyone with the public key can verify but not sign. That asymmetry is the whole point.

Now watch the attack. The attacker takes your published public key — often literally downloadable from a /.well-known/jwks.json endpoint — and crafts a token with "alg": "HS256". HS256 is symmetric: the same key signs and verifies. A vulnerable verifier reads alg from the attacker's header, sees HS256, and reaches for "the key" — which for this server is the RSA public key string. It then runs HMAC over the token using that public string as the secret. The attacker did the exact same computation. The signatures match. Forgery complete.

The root of the bug is a single line: letting the token's header decide which verification path to run. The token is the thing you do not trust yet, and you let it pick its own exam.

Weak secrets die offline

Even a correctly-pinned HS256 setup falls if the secret is guessable. HMAC verification is fast, which is a gift to attackers running an offline dictionary attack. They capture one valid token, then hash-and-compare millions of candidate secrets per second on a laptop — no network, no rate limit, no lockout.

# an attacker's whole workflow against a weak HS256 secret
hashcat -m 16500 captured.jwt rockyou.txt

secret, changeme, myapp2023, your company name — all fall in seconds. A real secret is 32+ bytes of cryptographic randomness, stored in a secrets manager, never in source control. If a token was ever signed with a weak secret, rotating to a strong one is the only fix; the old tokens are already forgeable.

Revocation: the problem nobody mentions in the tutorial

Here is the uncomfortable truth about stateless JWTs: you cannot un-issue one. A session cookie can be deleted server-side the instant a user logs out or an account is compromised. A signed JWT is valid until it expires, full stop — even if you fire the employee, even if the token leaked to an attacker five minutes ago.

There are three honest answers, and you pick based on your threat model:

Anyone who tells you stateless auth is free hasn't hit the revocation wall yet.

The fix

Defense here is boring on purpose. Pin the algorithm server-side — never let the incoming token choose it. Validate the claims (exp, aud, iss) on every request, not just the signature. Keep access tokens on short TTLs with rotating refresh tokens, and use a secret with real entropy.

jwt.verify(token, key, {
  algorithms: ["RS256"], // pin it — don't trust the header
  audience: "api.myapp.com",
  issuer: "auth.myapp.com",
  maxAge: "15m",
});

A few more rules that catch the long tail of bugs:

Why it keeps happening

The root cause is almost never the crypto — it is trusting attacker-controlled metadata. The token's header is data from a hostile source, yet libraries historically let it drive verification decisions. Treat every field before the verified signature as untrusted input, and most of these bug classes simply evaporate.

There is a deeper lesson for any authentication system, JWT or not: the security boundary is the moment you decide this request is who it claims to be. Everything that decision depends on — the algorithm, the key, the claims — has to be chosen by you, from trusted configuration, never read from the thing being authenticated.

The takeaway

Read more posts