JWT Security: 7 Common Mistakes Developers Make
JWT is easy to use and easy to misuse. A practical breakdown of the mistakes that have caused real-world breaches, with the fixes.
JWT (JSON Web Token) is the default choice for stateless authentication in modern web apps. It is also a format that has been behind real-world security breaches more than once. The spec itself is fine. The trouble is that the surface area invites small mistakes that turn into big problems.
Here are the seven patterns that keep showing up in security audits.
1. Accepting alg:none
Early JWT libraries did a terrible thing: they read the alg field from the token header and used it to decide how to verify the signature. That meant an attacker could set alg to "none" and submit a token with no signature at all. Some libraries happily accepted it.
This is fixed in every modern library, but it can come back if you write custom verification code. Always enforce an allowlist of algorithms on the server side, and reject anything else. Never let the token tell you how to verify itself.
2. Mixing up HS256 and RS256
HS256 uses a shared secret. Both the signer and the verifier need the same key. RS256 uses asymmetric crypto: the signer has a private key, verifiers use a public key. An attack from a few years ago involved taking a token signed with RS256 and re-submitting it marked as HS256, using the server's public key as the HMAC secret. If the server blindly trusted the alg header, it would happily verify.
Fix: the server picks the algorithm, not the token.
3. Putting sensitive data in the payload
JWT payloads are base64-encoded, not encrypted. Anyone who holds the token can decode it instantly using any JWT decoder. Do not put passwords, API keys, personal health data, or anything that shouldn't be visible in the clear into a JWT unless you're using JWE (JSON Web Encryption) on top.
Common practice is to put user ID, role, and expiration in the token, and fetch everything else from the database server-side.
4. Forgetting to check exp
The exp claim says when the token expires. Surprisingly often, code paths skip this check in test environments and never re-enable it in production. The easiest audit is to decode one of your issued tokens, add a day to the exp, craft a fake token with it, and see if your server accepts it. If yes, you have a bug.
Libraries usually check exp by default, but custom middleware is where this goes wrong.
5. Long-lived tokens
If exp is a year from now, then a stolen token is good for a year. Industry standard is short-lived access tokens (15 to 60 minutes) paired with a longer-lived refresh token that can be revoked server-side.
Short tokens limit damage. If you cannot revoke a JWT after issuing it (and you can't without maintaining a denylist), then short expiry is your only lever.
6. No audience or issuer validation
The iss (issuer) claim says who created the token. The aud (audience) claim says who it is intended for. If you have multiple services that all accept tokens signed by the same auth server, an attacker could steal a token intended for Service A and use it against Service B.
Always validate iss against your known list of trusted issuers, and aud against your own service identifier. Both are one-line checks that block a whole class of attack.
7. Storing tokens in localStorage
This one is contested. localStorage is accessible to any JavaScript running on the page, which means any XSS vulnerability becomes a token-exfiltration vulnerability. httpOnly cookies protect against that but open CSRF questions instead.
The common-sense answer: use httpOnly + Secure + SameSite=Strict cookies for session tokens when possible. If you must use localStorage, invest heavily in preventing XSS and use a strong Content Security Policy.
Testing your own JWTs
If you want to inspect a token from your own app right now, paste it into the JWT Decoder. The tool shows the exp and iat claims with auto-validation against the current time, flags alg:none and other red flags, and lets you verify HMAC signatures in the browser using the Web Crypto API. Nothing gets uploaded.
A five-minute audit of your own tokens often catches at least one of these mistakes before an attacker does.