Most JWT security problems don't come from a broken library. They come from a handful of small decisions made once during setup and never revisited: which algorithm to trust, where to store the token, what to actually validate on the way in.
Here are the ones that come up most often, and what to do instead.
1. Trusting the alg header instead of specifying it yourself
If your verification code reads the algorithm from the token rather than hardcoding what you expect, you're exposed to algorithm confusion attacks, including the classic alg: none bypass. Always tell your library which algorithm to expect. Never let the token decide for you.
2. Storing tokens in localStorage
Any script running on the page can read localStorage, including a compromised npm package or an XSS payload. HttpOnly cookies with Secure and SameSite flags aren't readable by JavaScript at all, which closes that door completely.
3. Long-lived access tokens with no revocation plan
A JWT can't be "deleted" once issued. If you're not thinking about this before you ship, you're one leaked token away from a bad week. Short expiry (15 minutes is a reasonable default) plus a refresh token you can revoke server-side covers most cases without needing a full blocklist.
4. Skipping claim validation beyond signature checks
A verified signature only proves the token wasn't tampered with. It says nothing about whether it's expired, meant for your service, or issued by who you think. exp, iss, and aud all need explicit checks, a valid signature on an expired or wrongly-scoped token is still a valid signature.
5. Putting sensitive data in the payload
JWT payloads are encoded, not encrypted. Anyone holding the token can read every claim inside it. Roles and IDs are fine. Passwords, full billing details, or anything you wouldn't want in a browser dev tools tab shouldn't be there.
6. Assuming RS256 is automatically safer than HS256
It depends on your setup, not the algorithm name. HS256 is fine for a single service verifying its own tokens. The moment multiple services need to verify independently, RS256's public/private key split is the right tool, but neither one is inherently "more secure" in isolation.
7. Never testing what happens when verification fails
Most teams test the happy path constantly and the failure path never. What does your app actually do with an expired token, a tampered signature, or a token signed by the wrong key? If you don't know the answer, that's worth finding out before an attacker does.
I wrote a longer version of this with code examples and a full FAQ section over on AuthParse: JWT Security Best Practices. If you're debugging a specific provider (Supabase, Clerk, Firebase, NextAuth), there's also a free JWT decoder that runs entirely client-side, nothing you paste is ever sent anywhere.
Curious what others have run into: which of these have actually bitten you in production, and which do you think is overrated as a risk?
Top comments (0)