JWT Misuse Patterns That Look Secure in Code Review.
Cybersecurity
JWTs are easy to use and easy to misuse. The misuse passes review because the code looks fine. Here are the patterns we find on most engagements that read as 'standard JWT' but break under attack.
By Arjun Raghavan, Security & Systems Lead, BIPI · March 12, 2026 · 6 min read
JWT is one of those technologies that is simpler than it looks and more dangerous than it reads. The library hides the cryptography, the verification call is a one-liner, and the resulting code looks correct in review. Several of the worst auth bugs we have found this year were JWT-related, and every one of them passed code review on the way in.
Pattern 1: Using the same key for signing and verifying across services
A team starts with one service that issues and verifies its own tokens. Symmetric HS256 with a shared secret is fine. Then they add a second service. The second service needs to verify tokens issued by the first. The team copies the secret. Now both services have the signing key. If service B is compromised, the attacker can mint tokens that service A will accept.
The fix is asymmetric (RS256 / EdDSA) where the issuer signs with a private key and verifiers only have the public key. Or move to opaque tokens with a centralised verification service. Either is more correct than 'every service knows the signing key.'
Pattern 2: Trusting the alg header
The classic. JWT verification reads the alg header and applies that algorithm. An attacker submits a token with alg: none and no signature. A naïve verifier accepts it. Most modern libraries fixed this. But: an attacker submits a token with alg: HS256 and uses the server's public RSA key as the HMAC secret. Any verifier that accepts both HS256 and RS256 falls for it.
Fix: pin the algorithm in the verifier, do not let the token tell you which algorithm to use.
Pattern 3: Long-lived tokens with no revocation
JWTs are stateless by design. That is the feature and the bug. If a token is issued for 30 days and the user is compromised, the token is valid for 30 days regardless of password reset, MFA enrollment, or account lockout. Most JWT systems do not have a revocation list because adding one defeats the stateless property.
What works in practice: short-lived access tokens (5-15 minutes) with a refresh token mechanism. The refresh token is checked against a stateful store on every refresh. Account lockout invalidates refresh tokens, which propagates to access tokens within minutes.
If your JWT TTL is measured in days, you are accepting that compromised tokens cannot be revoked. Make sure that is a deliberate choice.
Pattern 4: Storing JWTs in localStorage
Browser localStorage is accessible from any JavaScript on the page. One XSS, one compromised npm dependency, one rogue browser extension reading the page, and the token is exfiltrated. Most modern advice has moved to httpOnly Secure cookies, which the browser does not expose to JavaScript.
Cookie storage requires CSRF protection. The fix is SameSite=Strict on the auth cookie. This breaks one specific cross-site flow (clicking from email straight to authed page) which is usually acceptable.
Pattern 5: Putting authorization decisions in the JWT claims
Token contains role: 'admin'. The server reads the claim and acts on it. Token is valid for 24 hours. User's admin role is revoked. Server still treats them as admin until the token expires.
Authorization should be looked up server-side at request time, against a current source of truth. The JWT establishes identity (this is user X). The current authorization comes from your DB. Caching with a short TTL is fine. Reading from a stale token is not.
Pattern 6: Issuing JWTs without an audience claim
User logs into your platform. JWT is issued, no aud claim. User has access to two of your microservices: app.example.com and admin.example.com. The JWT works on both. The intention was that the user, when logged into the regular app, should not be able to talk to the admin service from the same token.
With aud, the issuer specifies which service the token is for. The verifier checks that the aud matches its own identity. A token issued for the regular app will not validate at the admin service. Without aud, you have one credential that works everywhere, which is rarely what you want for sensitive endpoints.
Closing
JWTs work fine in the cases they were designed for: stateless authentication of short-lived sessions on a single audience. Most production usage stretches that envelope: long-lived tokens, multiple audiences, embedded authorization, browser storage. Each of those is a defensible decision when made deliberately and a vulnerability when made by default. Code review does not catch these because the lines of code look fine. The bug is in what the lines do not say.
Read more field notes, explore our services, or get in touch at info@bipi.in. Privacy Policy · Terms.