Over the past six months we reported strikingly similar authentication weaknesses at three different clients. In all three, the JWT library was on its latest version, and in all three the problem was not the library itself but the verification options it had been given. In this post we look at where the weakness comes from, what it looks like in production and how to fix it for good, all from the defender’s side.
The short version
A JSON Web Token (JWT) works much like a session ticket that the server issues and signs. On every request, the server verifies the signature and trusts the user identity inside the ticket. The whole model rests on one assumption: the signature is always verified, under every condition, with the right key. Once verification is weakened, the contents of the ticket can no longer be trusted.
These were the three misconfigurations we ran into most often.
1. Reading the algorithm from the token
The verification code decided which algorithm to use by looking at the token’s own header. The safe approach is the exact opposite: the server pins the algorithm it expects and enforces it, no matter what the token claims.
Rule: pass the accepted algorithms to your verification function as an explicit list. Never trust the algorithm a token proposes.
2. Mixing up symmetric and asymmetric keys
In some applications, verification was done with an asymmetric public key, but the code had been left loose enough to also allow symmetric verification. A public key is, by definition, public; once symmetric verification is allowed, that public value ends up in a position where it can stand in for the signing secret.
The fix is clear-cut: each endpoint should accept exactly one algorithm family. If you use the RS/ES family, reject the HS family outright at the verification layer.
3. Missing expiry and revocation checks
Even with a valid signature, accepting a ticket that has expired or was revoked at logout is a separate problem. We saw cases where the exp claim was never checked and tokens were not invalidated on the server side after the user logged out.
How we closed it
At all three clients the fix was small but decisive:
- The accepted algorithms were passed to the verification call as an explicit list.
- Every algorithm family other than the expected one was rejected.
- The
exp,issandaudclaims were made mandatory parts of verification. - On critical privilege changes, a new token was issued and the old one was invalidated on the server side.
A checklist for your own team
- Do you pass the list of algorithms to the verification function, or does the token decide?
- If you sign with a public/private key pair, does your verification layer strictly reject the symmetric family?
- Are the expiry, issuer and audience claims checked on every request?
- Do old session tokens actually become invalid on logout and password change?
If you can’t answer “yes” to all four of these questions, your authentication layer deserves a focused review. This is exactly the chain we walk through in our penetration tests.