A JSON Web Token is a small string carrying signed claims — "this user is sub=42, valid until exp=..., issued by iss=...". They power most modern auth systems. They're also the format with the longest list of "we did it wrong" stories. The format is fine; the way developers validate it often isn't.
header.payload.signature. Joined by dots.RS256, HS256, ES256, ...) and key ID (kid).iss issuer, sub subject, aud audience, exp expiry, nbf not-before, iat issued-at, jti token ID. Plus your custom claims.jwt.io); only the holder of the signing key can produce a valid signature. JWE is the encrypted variant — rarely used.alg: none — The Original DisasterThe JWT spec includes "alg": "none" meaning "no signature." Several libraries, in their early days, accepted it — passing an unsigned token through validation as if it were valid. Anyone with a text editor became an admin.
Defense: pin the expected algorithm. Don't accept whatever the token's header claims. Use libraries that require an explicit algorithm whitelist.
If the verifier accepts whatever alg the token claims, an attacker can take a token signed with RS256 (asymmetric), change alg to HS256 (symmetric), and sign it using the public key as the HMAC secret. Some libraries did this. The fix is the same: pin the algorithm.
Verifying the signature is necessary; it's not sufficient. Always validate:
iss matches the expected issuer.aud matches your service.exp in the future, nbf in the past.iat reasonable (not far-future).A signed token from another service that hasn't checked these is still an attack vector when accepted by yours.
JWT payloads are signed, not encrypted. Anyone who sees the token can decode the claims. PII, internal IDs, sensitive flags shouldn't be there unless you're using JWE (encrypted JWT).
A compromised JWT is valid until exp. Without server state you can't revoke it. Three mitigations:
jtis for high-value scenarios (logout, password change).localStorage is XSS-readable. One DOM XSS leaks every token in every session. Prefer HttpOnly cookies for browser-stored tokens, paired with a backend-for-frontend that proxies API calls. Modern guidance is unambiguous on this.
kid Without ConstraintThe kid header tells the verifier which key to use. Some libraries used it as a path or DB lookup — and attackers smuggled SQLi or path traversal through it. Treat kid as opaque, look it up against a fixed JWKS, never as a path.
For browser sessions to your own backend, opaque session IDs in HttpOnly cookies are simpler and easier to revoke than JWTs. JWTs shine when verification needs to happen without calling the issuer — across services, at the edge, or in a federated context.
Don't pick JWT because it's "stateless and cool." Pick it when stateless verification is a real requirement; otherwise stick with sessions and save yourself a category of bugs.