Cryptography

JWT Security Pitfalls: 7 Mistakes That Break Your Authentication

10 min read
By
JWT Security Pitfalls: 7 Mistakes That Break Your Authentication

Photo by Tima Miroshnichenko from Pexels

JSON Web Tokens are one of the most popular authentication mechanisms in modern web development. They're stateless, scalable, and work across services and domains. But their flexibility is also their Achilles' heel. JWTs can be configured in dozens of ways, and many of those configurations are dangerously insecure.

We've audited dozens of JWT implementations, and the same mistakes appear over and over. Here are the seven most dangerous JWT security pitfalls, why they happen, and how to fix them.

Pitfall 1: The "alg: none" Attack

This is the most infamous JWT vulnerability, and it's still present in production systems. The attack exploits a fundamental design flaw in the JWT specification combined with permissive library implementations.

The JWT header includes an alg field that specifies which algorithm was used to sign the token. The specification allows this field to be set to none, which means the token is unsigned. This was intended for situations where integrity is guaranteed by other means, but it creates a massive security hole if your library accepts none as a valid algorithm.

An attacker creates a token with alg: "none", removes the signature portion, and sends it to your server. If your verification code doesn't explicitly require a specific algorithm, the library sees none and skips verification entirely. The token is accepted as valid.

The fix is simple but critical: always specify the expected algorithm when calling your verification function. Never let the token's header dictate how it should be verified. In most libraries, this means passing the algorithm as a parameter:

// WRONG: trusts the token's alg field jwt.verify(token, secret)

// RIGHT: explicitly requires HS256 jwt.verify(token, secret, { algorithms: ['HS256'] })

This single line of code prevents the none attack and several related algorithm confusion attacks.

Pitfall 2: Weak or Hardcoded Secrets

The second most common pitfall is using a weak secret. We've seen JWT secrets that are literally the string "secret", "key", "changeme", or the application name. These are not secrets, they're placeholders.

A weak secret can be brute-forced or guessed. Tools like jwt-cracker can try millions of secrets per second using GPU acceleration. A 6-character alphanumeric secret can be cracked in minutes. Even a 12-character human-readable string falls to dictionary attacks combining common words and patterns.

The fix is to use a cryptographically strong random secret of at least 256 bits (32 bytes). Generate it with a CSPRNG, not by typing on a keyboard. Our JWT Secret Generator produces cryptographically secure secrets in hex or base64url format using the browser's native CSPRNG.

For a comprehensive guide to secret management, see our JWT secret key best practices.

Pitfall 3: Algorithm Confusion (HS256 to RS256)

This pitfall affects systems that use asymmetric algorithms like RS256. In RS256, the server signs tokens with a private key and verifies them with a public key. The public key is, by definition, public.

The attack works when your verification code doesn't pin the expected algorithm. An attacker takes your public key (which they can obtain from your JWKS endpoint or other public sources) and uses it as an HMAC secret. They create a token signed with HS256 using the public key as the secret, but set the alg header to HS256 instead of RS256.

If your library trusts the alg header, it sees HS256, treats the public key as an HMAC secret, and verifies the token successfully. The attacker has forged a token using only public information.

The fix is the same as for the none attack: always specify the expected algorithm. If you're using RS256, require RS256 during verification. Never accept HS256 tokens in a system designed for RS256.

Pitfall 4: Sensitive Data in the Payload

JWT payloads are not encrypted. They are base64url-encoded, which is a form of encoding, not encryption. Anyone who can read the token can decode the payload. This includes the user's browser, any proxy or middleware the token passes through, and anyone who intercepts the token in transit.

Despite this, we regularly see JWT payloads containing passwords, email addresses, phone numbers, social security numbers, and other sensitive data. This is a serious data breach waiting to happen.

The payload should contain only what's necessary for authorization: a subject identifier (user ID), role or scope claims, and standard claims like expiration and issuer. If you need to include sensitive data, encrypt it separately before placing it in the payload, or store it server-side and reference it by ID.

Pitfall 5: Missing or Excessive Expiration

Every JWT should have an exp claim that specifies when the token expires. Tokens without expiration are valid forever, which means a stolen token grants permanent access.

We see two opposite mistakes here. Some developers forget to set exp entirely, creating tokens that never expire. Others set excessively long expiration times, like 30 days or a year, which is almost as bad.

The right expiration depends on the token type. Access tokens should be short-lived: 15 minutes to 1 hour. Refresh tokens can be longer: days to weeks. The shorter the access token, the less damage a stolen token can do. Use refresh tokens to provide a good user experience without long-lived access tokens.

Pitfall 6: Not Validating the Issuer and Audience

JWT includes iss (issuer) and aud (audience) claims that help prevent token misuse across different services. The iss claim identifies who issued the token, and the aud claim identifies who the token is intended for.

If you don't validate these claims, a token issued for one service can be used on another. In a microservices architecture, this means a token for the user profile service can access the billing service. In a multi-tenant system, a token from one tenant can access another.

The fix is to always validate iss and aud during verification. Specify the expected issuer and audience in your verification options. Reject tokens that don't match.

Pitfall 7: Storing Tokens Insecurely

Where you store the JWT on the client side matters. The two common options are localStorage and cookies, and each has different security implications.

localStorage is accessible to any JavaScript running on the page. If your application has an XSS vulnerability, an attacker's script can steal the token from localStorage. This is a significant risk because XSS vulnerabilities are common and often hard to eliminate entirely.

Cookies with the HttpOnly flag are not accessible to JavaScript, which protects against XSS-based token theft. However, cookies are automatically sent with every request, which makes them vulnerable to CSRF attacks. You need to implement CSRF protection (like the SameSite flag or anti-CSRF tokens) when using cookies.

The right choice depends on your application. For most web applications, HttpOnly cookies with proper CSRF protection offer better security than localStorage. For APIs consumed by mobile apps or SPAs with a separate backend, localStorage might be more appropriate.

Bonus: Not Rotating Secrets

We said seven pitfalls, but here's one more that's so common it deserves mention: never rotating your JWT secret. If your secret is the same today as it was when the application was first deployed, you're carrying unnecessary risk. Secrets should be rotated regularly to limit the impact of potential compromise.

Rotation doesn't have to be disruptive. With a dual-secret grace period strategy, you can rotate secrets without logging out any users. See our guide on zero-downtime JWT secret rotation for a complete walkthrough.

The Bottom Line

JWT is a powerful and flexible authentication mechanism, but that flexibility creates many opportunities for misconfiguration. The seven pitfalls in this guide are all common, all dangerous, and all fixable with relatively simple changes.

Always specify the expected algorithm during verification. Use strong, randomly generated secrets. Never put sensitive data in the payload. Set reasonable expiration times. Validate the issuer and audience. Store tokens securely. Rotate secrets regularly.

If you're not sure whether your current implementation has any of these issues, now is the time to audit. Generate a new, strong secret with our JWT Secret Generator, review your verification code against this list, and fix any issues you find. The security of your authentication system depends on getting these details right.

Frequently Asked Questions

What is the JWT "alg: none" attack?

The "alg: none" attack exploits JWT libraries that trust the algorithm field in the token header. An attacker sets the algorithm to "none", removes the signature, and the library accepts the token without verification. The fix is to always specify the expected algorithm during verification and never trust the token’s own header.

Are JWTs encrypted?

No, by default JWTs are not encrypted. The header and payload are base64url-encoded, which is encoding, not encryption. Anyone who obtains a token can decode and read the payload without the secret. If you need confidentiality, you must encrypt the payload separately or use JWE (JSON Web Encryption).

What is the most common JWT security mistake?

Using a weak or hardcoded secret is the most common mistake. Many developers use short, human-readable strings like "secret" or "myKey123" instead of cryptographically random bytes. These secrets can be brute-forced in seconds, allowing attackers to forge tokens.

Should I store JWTs in localStorage or cookies?

Cookies with the HttpOnly and Secure flags are generally safer for storing JWTs because they protect against XSS attacks. localStorage is accessible to any JavaScript running on the page, including malicious scripts from XSS vulnerabilities. However, cookies require careful CSRF protection. Neither is perfect; the choice depends on your threat model.

Try NovelCrypt Tools

Experience military-grade encryption for your sensitive data. Create self-destructing messages, encrypt files, or explore our experimental lab tools.

Explore NovelCrypt