Home/Blog/JWT Security Mistakes That Put Your API at Risk
Web Development
JWT Security Mistakes That Put Your API at Risk
AJAjish StephenAugust 8, 20267 min read
JWT is easy to get working and surprisingly easy to get wrong in ways that don't show up until someone actually tries to exploit them. These four mistakes account for most of the real-world JWT vulnerabilities I see, across every language and framework — the framework isn't the risk, the implementation choices are.
Mistake 1: Algorithm confusion
A JWT's header declares which algorithm was used to sign it. If your server trusts that header blindly instead of enforcing an expected algorithm, an attacker can manipulate it — in the worst documented cases, tricking a server expecting an asymmetric algorithm (RS256) into verifying a forged token with its own public key treated as an HMAC secret.
⚠Always explicitly specify the expected algorithm(s) when verifying a token, and reject anything that doesn't match. Never let the token's own header dictate which algorithm your server uses to verify it.
// ✗ Risky — no algorithm restriction, trusts the token's own header jwt.verify(token, secretKey);
Mistake 2: A weak or shared signing secret
If your secret is short, guessable, or reused across environments, an attacker who obtains or brute-forces it can forge any token they want. Generate a genuinely random, long secret, and use different secrets for staging and production:
opensslrand -base64 64
Store the result as an environment variable, never as a literal string in source code — the same rule that applies to any credential.
Mistake 3: Sensitive data in the payload
A JWT payload is base64-encoded, not encrypted. Anyone who has the token can decode it instantly:
// ✗ Never put this in a JWT payload { "userId": 42, "ssn": "123-45-6789", "creditCard": "4111..."}
Mistake 4: No expiry, or an expiry that's too long
A token with no exp claim is valid forever — a stolen token becomes permanent access rather than a limited window. Short-lived access tokens (15–60 minutes) combined with a longer-lived refresh token is the pattern to actually use, the same one covered in an earlier post on JWT authentication in Laravel — the principle applies identically regardless of which framework issues the token.
A quick self-check
→Does your verification step explicitly restrict the accepted algorithm?
→Is your signing secret long, random, and different between environments?
→Would you be comfortable if the payload contents were posted publicly?
→Does every issued token have a short, sensible expiry?
Want a real security review of your authentication system, whatever framework it's built on? This is part of what I cover in DevOps services engagements.
Common questions
What is algorithm confusion in JWT and why is it dangerous?
It happens when a server verifies a token using whatever algorithm the token itself claims, without restricting to an expected algorithm. This can let an attacker forge a valid-looking token.
Is it safe to store a JWT in localStorage?
It works, but it is more exposed to theft through cross-site scripting than an httpOnly cookie, since any script running on the page can read localStorage.
Should a JWT ever contain sensitive data in its payload?
No. A JWT's payload is base64-encoded, not encrypted — anyone who has the token can decode and read it in seconds.
What happens if a JWT never expires?
A stolen token remains valid forever. Every JWT should include an exp claim, with short-lived access tokens paired with a refresh token.
Is your authentication actually secure?
I review real authentication implementations for these exact gaps.