JWT & key confusion

In Java, you'd deserialize a signed object with the verification method fixed in the calling code — the bytes on the wire never get a vote in how they're checked. A JWT is different in a way that catches Java engineers off guard: the token's own header carries alg, and a verifier that reads it and dispatches on it is letting the attacker's forgery choose how it gets checked. Every attack on this page is that one design mistake wearing a different costume.

alg=noneRS256→HS256 confusion exp / nbfconstant-time compare kid is a dict key, not a path

Five tokens, one verifier — and a second, unsafe one for comparison

A compact JWT is three base64url segments — header.payload.signature — joined by dots. Every token below is a real token: built the same way sec-jwt's checks build theirs, with a real HMAC-SHA256 signature where one applies. Pick a scenario and the scene decodes it and runs it past two verifiers: the safe one (checks the located key's own kind before ever using it, checks exp/nbf, rejects alg=none unconditionally) and a naive one (trusts whatever the header claims). They agree on an honest token. They disagree on every attack — and the naive one is not a strawman, it is the shape of the real CVEs this class of bug has produced.

Why the naive verifier is not a strawman

"Read alg from the header, then verify with it" is the first working version almost everyone writes, because it is the first version that makes the happy path pass. The fix is not a smarter parser — it's moving the decision out of the token's hands: the caller names the algorithms it will accept (allowed_algs), alg=none is rejected unconditionally, and the key you already hold for a given kid carries its own kind ("hmac" or "rsa") that the verifier checks before it will use that key at all — so a public key handed back as an HMAC secret is caught by the key's own record, not by hoping the header is honest.

Takeaways: never let the token pick its own verification method — alg is attacker-controlled input, and an allow-list the caller sets is the only algorithm choice that is trustworthy. Reject alg=none unconditionally, not "unless it's in the allow-list". Every key you hold has a kind; check it before using the key, so an RSA public key can never be handed to HMAC as a secret. Check exp and nbf on every verification, not just at issue time. Compare signatures with hmac.compare_digest, never ==. Treat kid as an opaque dict key — normalizing or path-resolving it turns a lookup into a bypass.