Six Ways Production JWT Goes Wrong
valkant/May 2026
JWT is one of those technologies where the specification is fine and the implementations keep finding new ways to break. We test enough authentication flows that a pattern is clear. The same six classes of failure show up over and over, in companies of every size, in stacks written in every language. If you ship JWT-based authentication, this is your audit checklist. If you hunt JWT-based authentication, this is your hunting checklist.
The first failure is the HS256 secret that is not actually a secret. We have cracked production JWT signing secrets with hashcat in under four minutes more than once. The secret was a dictionary word. The secret was the company name in lower case. The secret was the literal string "secret" in an early-stage project that grew up without ever rotating it. Treat the signing secret as a password and run it through the same generation rules. If the secret has fewer than 32 random bytes, it is in scope for offline cracking.
The second failure is the algorithm-confusion attack. The verifier accepts both RS256 and HS256 without enforcing which one a given token should use. The public RSA key, which is supposed to be public, becomes the HMAC secret. The attacker signs a new token using HS256 with the public key as the shared secret and the server happily verifies it. This is a decade-old class and we still find it. Libraries that auto-detect the algorithm from the token header are the usual cause.
The third failure is the kid header. The kid identifies which key to use for verification. Some implementations look that key up out of a database or filesystem path constructed from the kid value. Path traversal in the kid lets an attacker point the verifier at /dev/null and sign tokens with an empty key. SQL injection in the kid lets an attacker return arbitrary key material from the database. Anything user-controlled that touches the verification path is exploitable until proven otherwise.
The fourth failure is missing or mis-applied expiration checks. The token has an exp claim. The server never checks it. Or the server checks exp on the access token and not on the refresh token. Or the server checks expiration but caches the verification result for longer than the expiry window. The consequence is the same. Old tokens that should be dead are still alive. Tokens from former employees, stolen sessions, and revoked devices all keep working.
The fifth failure is the none algorithm. Modern libraries reject it. Older libraries pinned to old versions still accept it. Internal services that strip and re-sign tokens between hops sometimes forget to validate the algorithm on the inbound side because they assume the outer gateway already did. We have seen this in microservice architectures where the gateway team and the service team had different mental models about who owns verification. The token went through with alg set to none and no signature at all.
The sixth failure is the one nobody writes about. Claims that the application trusts blindly because the framework verified the signature and stopped there. A signed token can still contain a user_id that does not belong to the authenticated user, a role claim the issuer never granted, or a tenant scope that escalates across customers. Signature verification proves the token has not been tampered with after issuance. It does not prove the issuer made good choices about what to put inside. Audit the claims, not just the signature.