JWT security is one of the most misunderstood areas of web application security, and implementation mistakes in JSON Web Token handling are among the most common findings in security audits. Whether you’re building a REST API, a single-page application, or a microservices architecture, getting JWTs wrong can expose authentication entirely – not just weaken it.
Why JWT Mistakes Are More Dangerous Than They Look
A misconfigured JWT implementation doesn’t just create a small gap. It can allow attackers to forge tokens, impersonate any user, and bypass authentication completely – without ever touching a password.
Unlike many vulnerabilities that require chaining multiple steps, some JWT flaws are one-step exploits. The algorithm confusion attack, for example, lets an attacker craft a valid-looking token that a server will accept as legitimate.
The reason these bugs survive is that JWT libraries often try to be flexible and developer-friendly, which can backfire when the developer doesn’t understand what the library is doing under the hood.
The Algorithm Confusion Attack
This is one of the most severe JWT implementation mistakes and still shows up regularly. The vulnerability works like this: a server uses RS256 (asymmetric signing), and the public key is available. An attacker takes that public key, changes the algorithm in the token header to HS256 (symmetric), and signs the token with the public key as the HMAC secret. If the server blindly trusts the alg header from the incoming token, it will verify the signature using the public key as an HMAC secret – and it will pass.
The fix is simple but critical: always specify and enforce the expected algorithm server-side. Never let the client dictate which algorithm to use.
Some libraries have had this exact flaw in their default behavior. Always check your JWT library’s changelog and known CVEs before upgrading or deploying.
Weak Secrets and Hardcoded Keys
For HMAC-based JWTs (HS256, HS384, HS512), the security of the token depends entirely on the strength of the secret key. Using a short, predictable, or hardcoded secret is equivalent to using a weak password on the signing process.
Secrets like secret, password123, or dev-key are crackable offline. Once an attacker captures a valid JWT, they can run dictionary or brute-force attacks against the secret without ever touching the server – there’s no rate limiting because it’s all local computation.
Use secrets of at least 256 bits, generated with a cryptographically secure random number generator. Store them in environment variables, not in source code. Rotate them periodically, and have a plan for emergency rotation if a secret is ever leaked.
Missing or Excessively Long Expiration
Tokens without an exp claim are valid forever. That sounds like an edge case, but it appears in real applications more than it should – especially in internal tools and prototypes that quietly made it to production.
Even when expiration is set, the window is often far too generous. A JWT with a 30-day expiration gives an attacker who steals a token a full month of valid access. For most use cases – user sessions, API access, mobile app authentication – tokens should expire in minutes to hours, not days.
The counterargument is user experience: short-lived tokens mean more friction. The right answer is combining short-lived access tokens with long-lived refresh tokens, stored with proper security controls.
Where You Store the Token Matters as Much as How You Sign It
A technically perfect JWT can still be compromised through storage mistakes. The two common choices – localStorage and cookies – each carry different risk profiles.
Storing JWTs in localStorage is straightforward to implement, which is why it’s popular. But any XSS vulnerability on your page can read localStorage content directly. An attacker who injects a script can exfiltrate every token in storage without any browser protection stopping them.
HttpOnly cookies protect against this specific attack because JavaScript cannot read them. The trade-off is CSRF risk, which you mitigate with SameSite cookie attributes and CSRF tokens. Neither approach is inherently better – the right choice depends on your threat model, but the decision should be deliberate, not accidental.
Skipping Claim Validation
Signature verification alone isn’t enough. A valid signature proves the token hasn’t been tampered with – it doesn’t prove the token is appropriate for this request.
Claims like iss (issuer), aud (audience), and sub (subject) need to be validated server-side on every request. Without audience validation, a token issued for one service can be replayed against another. Without issuer validation, a token from a completely different system might be accepted.
This is especially relevant in microservices architectures, where multiple services share similar token formats. API security depends on each service independently validating that the token was intended for it, not just that it’s structurally valid.
The Myth: JWTs Are Encrypted
One of the most persistent misconceptions is that JWTs protect the data inside them. They don’t – at least not by default. A standard JWT (JWS – JSON Web Signature) is only signed, not encrypted. The payload is base64url-encoded, which is trivially reversible.
Anyone who intercepts or reads a JWT can decode the payload in seconds without any key. This means sensitive data – roles, email addresses, internal IDs, permission flags – should not be stored in JWT claims unless you’re using JWE (JSON Web Encryption), which is a separate and more complex standard.
This trips up developers who see a long, opaque string and assume it’s meaningless to anyone without the secret. The secret is only needed to forge a token – reading it requires nothing.
No Revocation Strategy
JWTs are stateless by design, which means there’s no built-in way to invalidate a token before it expires. Log out a user? The token is still valid until expiry. Detect a compromised account? You can’t kill existing tokens without extra infrastructure.
For sensitive applications, you need a token revocation list or a short expiration window – or both. A common pattern is keeping a server-side denylist of revoked JTI (JWT ID) claims, checked on each request. This reintroduces some statefulness, but it’s unavoidable when you need immediate revocation capability.
Frequently Asked Questions
Can a JWT be tampered with if the attacker doesn’t have the signing key?
A properly signed JWT cannot be tampered with without invalidating the signature – any change to the payload will cause verification to fail. However, attackers can exploit implementation flaws like algorithm confusion to forge tokens without the key, which is why strict server-side validation matters more than the token format itself.
Is it safe to store user roles and permissions in a JWT?
Storing roles in JWTs is common and generally acceptable, but remember the payload is not encrypted – anyone with the token can read those claims. More importantly, because JWTs are stateless, permission changes don’t take effect until the token expires. For high-sensitivity permission changes like revoking admin access, combine short expiry with a revocation mechanism.
What’s the most common JWT mistake found in real security audits?
Algorithm confusion and hardcoded secrets appear most frequently. A close third is trusting user-supplied data from JWT claims without re-validating it against the database – for example, using a role claim directly to authorize actions without checking whether that role is still valid for that user.
Getting JWT Implementation Right
JWT security isn’t about choosing the right library – it’s about understanding what the library actually does and explicitly configuring every security-relevant option. Fix the algorithm to a specific value server-side. Use strong, rotatable secrets. Set short expiration windows. Validate every claim that matters. Store tokens with the appropriate protection for your threat model.
Most JWT vulnerabilities aren’t exotic. They’re the result of using defaults that prioritize flexibility over security. Treating each configuration option as a deliberate choice – rather than something to skip past – closes the majority of real-world attack surface.
