7 JWT Security Mistakes and How to Avoid Them
JSON Web Tokens (JWTs, defined in RFC 7519) are everywhere in modern authentication: OAuth access tokens, OpenID Connect ID tokens, and countless home-grown session systems. The format is simple, but the security of a JWT depends entirely on how the receiving server checks it, and the same handful of mistakes keeps appearing in real vulnerabilities. Here are seven of them, with what goes wrong and how to fix each one.
A 30-second refresher
A JWT is three Base64URL-encoded parts joined by dots: header.payload.signature. The header names the signing algorithm (for example "alg": "HS256"), the payload holds claims such as the user ID and expiry time, and the signature proves the first two parts were produced by someone holding the key. Paste any token into our JWT decoder and you will see the header and payload in plain JSON — which leads straight to the first mistake.
1. Treating the payload as secret
A signed JWT is encoded, not encrypted. Anyone who sees the token can read every claim without any key; Base64URL is just a text representation (try it in our Base64 decoder). Tokens end up in browser storage, logs, proxies, and error reports, so anything in the payload should be considered visible.
Fix: put only identifiers and authorization data in the payload — never passwords, personal data you would not show the user, or internal secrets. If the contents must be confidential, use an encrypted JWE instead, or keep the data server-side and put only a reference in the token.
2. Decoding instead of verifying
Many JWT libraries offer both a decode function, which just parses the token, and a verify function, which checks the signature. Using decode in an authentication path means any user can edit their token — change "role": "user" to "role": "admin" — and the server will believe it.
Fix: every request that relies on a token must call the verifying function with the expected key. Treat unverified decoding as a debugging tool only, which is exactly what our decoder is for.
3. Accepting alg: none
The JWT specifications define an “unsecured” algorithm called none, meaning no signature at all. If a server reads the algorithm from the token header and its library honors none, an attacker can strip the signature, set "alg": "none", and forge any claims they like. Several libraries shipped with this behavior in the past.
Fix: never let the token choose its own algorithm. Configure the verifier with an explicit allow-list, such as algorithms: ['RS256'], and reject anything else.
4. Algorithm confusion (RS256 vs HS256)
With RS256 the issuer signs with a private key and verifiers check with the public key. With HS256 the same shared secret both signs and verifies. If a server expects RS256 but lets the header pick the algorithm, an attacker can send a token marked HS256 and signed using the server’s public key as the HMAC secret. A vulnerable library then “verifies” it with that same public key, and the forgery passes. The public key is, by design, public.
Fix: the same allow-list from mistake 3. Bind each key to exactly one algorithm, and use libraries that refuse to use an asymmetric public key as an HMAC secret.
5. Weak HMAC secrets
An HS256 token is effectively a hash of its contents keyed with your secret. Anyone holding a single valid token can try guesses for the secret offline, at GPU speeds, with no interaction with your server (the same economics described in how password cracking works). Secrets like secret, changeme, or the app name are found in seconds, after which the attacker can mint valid tokens for any user. RFC 7518 requires an HMAC key at least as long as the hash output — 256 bits for HS256.
Fix: generate the secret randomly with at least 256 bits (32 random bytes, or 64 hex characters) using our random token generator or your platform’s CSPRNG, keep it out of source control, and rotate it if it may have leaked. Better still, if multiple services need to verify tokens, use an asymmetric algorithm so verifiers never hold a signing key.
6. Not checking the claims
A valid signature only proves who issued the token. It does not prove the token is meant for you, or still valid. Common gaps:
exp/nbf: expired or not-yet-valid tokens accepted because the time claims were never checked.aud: a token issued for one API accepted by another API that trusts the same identity provider.iss: tokens from an unexpected issuer accepted because the key lookup was too permissive.
Fix: configure your verifier with the expected issuer and audience and let it enforce expiry, allowing at most a small clock skew (a minute or two). Keep access tokens short-lived — minutes, not days.
7. Storing and revoking tokens carelessly
Two operational mistakes round out the list. First, storing long-lived tokens in localStorage exposes them to any cross-site scripting bug on the page; an HttpOnly, Secure, SameSite cookie keeps them out of reach of scripts (paired with CSRF protection). Second, a JWT stays valid until it expires, even after the user logs out or changes their password, because the server does not look anything up.
Fix: issue short-lived access tokens together with a refresh token that the server can revoke. For high-risk actions, check a server-side session or a deny-list keyed on the token’s jti claim.
A verification checklist
- Signature verified on every request, with an explicit algorithm allow-list.
- HMAC secrets of 256+ random bits, or asymmetric keys.
exp,nbf,iss, andaudall enforced.- No secrets or sensitive personal data in the payload.
- Short-lived access tokens, revocable refresh tokens, and storage out of reach of page scripts.
Key takeaways
- Anyone can read a JWT; only verification makes it trustworthy.
- The server, not the token, decides which algorithm is acceptable.
- Most JWT vulnerabilities are configuration mistakes that a mature library prevents when set up with explicit algorithms, issuer, audience, and expiry.