In the fast-paced world of web and mobile development, JWT (JSON Web Tokens) has become the de facto standard for managing authentication and authorization. It’s stateless, easy to implement, and widely supported across frameworks. But here's the problem: JWTs are not bulletproof by default—and small mistakes can easily turn into huge vulnerabilities.
If you're a developer using JWTs in your Node.js, Express, Python, or even mobile app backends—this post is for you.
1. Using "none" as the Algorithm (alg)
The Mistake:
One of the most infamous JWT vulnerabilities involves the use of "alg": "none"
—which essentially tells the JWT validator: “Hey, don’t bother verifying the signature!”
Some insecure JWT libraries or misconfigured setups will accept such tokens and treat them as valid.
Why It’s Dangerous:
If an attacker crafts a token and sets "alg": "none"
, and your server accepts it without verifying, you’ve given them admin access.
How to Fix It:
- Explicitly define which algorithms are allowed in your JWT validation library.
- Disable or disallow the
none
algorithm. - In
jsonwebtoken
(Node.js), do this:
jwt.verify(token, secretKey, { algorithms: ['HS256'] });
2. Storing JWTs in LocalStorage
The Mistake:
Many frontend tutorials suggest storing JWTs in localStorage
. It's easy, it works—and it's a huge security risk.
Why It’s Dangerous:
LocalStorage is vulnerable to XSS (Cross-Site Scripting). If an attacker injects malicious JavaScript into your site, they can read tokens and impersonate users.
How to Fix It:
- Prefer HTTP-only, Secure cookies to store JWTs. These can’t be accessed by JavaScript.
- Always sanitize and validate user input to prevent XSS attacks.
// Example of setting JWT in a secure cookie (Node.js + Express)
res.cookie('token', jwtToken, {
httpOnly: true,
secure: true, // only over HTTPS
sameSite: 'Strict',
});
3. Not Setting an Expiry (exp
) Claim
The Mistake:
JWTs are stateless—meaning once they’re issued, they remain valid until they expire. If you forget to set an expiry time, the token lives forever.
Why It’s Dangerous:
Long-lived tokens can be stolen and reused indefinitely. This increases the attack surface.
How to Fix It:
Always set a reasonable expiration time. Examples:
- Access tokens: 15 minutes
- Refresh tokens: 7 days
jwt.sign(payload, secret, { expiresIn: '15m' });
Also consider using short-lived tokens + refresh token rotation for enhanced security.
4. Using Weak or Predictable Secrets
The Mistake:
JWTs use secrets (in HS256, for example) to sign and verify tokens. But many developers use short or guessable secrets like "mysecret"
or "1234"
.
Why It’s Dangerous:
An attacker could brute-force your secret and generate valid tokens themselves. That’s game over.
How to Fix It:
- Use long, random secrets (e.g., at least 256 bits).
- Store secrets securely using environment variables.
- For production, generate secrets using
openssl
or password managers.
openssl rand -hex 32
And never hardcode your secret in code like:
const secret = 'superweaksecret'; // ❌ Don't do this!
5. Accepting Tokens Signed with Different Algorithms
The Mistake:
Some JWT libraries auto-detect the algorithm used in the incoming token. So even if you usually use RS256
, the library might still accept a token signed with HS256
if it sees one.
Why It’s Dangerous:
This opens the door to algorithm confusion attacks. Attackers could forge a token with HS256
and sign it using your public key as the secret.
How to Fix It:
- Explicitly declare accepted algorithms.
- Validate that the JWT algorithm matches your expectation.
jwt.verify(token, publicKey, {
algorithms: ['RS256'], // explicitly set
});
6. Trusting JWTs Without Validation
The Mistake:
Some devs decode JWTs without verifying the signature at all—assuming if it looks okay, it is okay.
const payload = jwt.decode(token); // Insecure: no validation
Why It’s Dangerous:
Decoding does not verify that the token is legitimate or untampered. Anyone could change the payload and pass it off as valid.
How to Fix It:
- Use
jwt.verify()
to decode and validate the token. - Always ensure the token hasn’t been altered and is from a trusted issuer.
7. Exposing Sensitive Data in the Payload
The Mistake:
JWT payloads are base64-encoded—not encrypted. Many developers mistakenly put passwords, personal data, or even credit card info in them.
Why It’s Dangerous:
Anyone who gets access to the JWT (e.g., via browser storage or logs) can decode and read the payload.
How to Fix It:
- Only include non-sensitive, minimal data in JWTs (e.g., user ID, roles).
- If you need to transmit sensitive data, encrypt it separately.
- Consider using JWE (JSON Web Encryption) if encryption is absolutely necessary.
8. Not Rotating Refresh Tokens
The Mistake:
You might issue long-lived refresh tokens to allow users to get new access tokens. But if you're not rotating them (i.e., issuing a new one each time), you’re vulnerable to replay attacks.
Why It’s Dangerous:
If a refresh token is stolen, it can be reused indefinitely unless you have token rotation in place.
How to Fix It:
- Implement refresh token rotation: each time a new access token is issued, also issue a new refresh token and invalidate the old one.
- Store refresh tokens in a database with metadata (user ID, expiry, last used).
- Invalidate old tokens immediately if reuse is detected.
9. Skipping Audience (aud
) and Issuer (iss
) Claims Validation
The Mistake:
You receive a JWT, decode it, and trust the payload—but you don’t validate who issued it or who it was intended for.
Why It’s Dangerous:
Anyone could issue a JWT from another system or fake app, and your backend might accept it blindly.
How to Fix It:
- Always validate both
aud
(audience) andiss
(issuer) claims to ensure the token is meant for your app and issued by your auth server.
jwt.verify(token, secret, {
audience: 'your-app',
issuer: 'your-auth-server',
});
10. Over-relying on JWT for Session Management
The Mistake:
Some devs treat JWTs like traditional sessions—logging users out by “deleting the token” on the frontend.
Why It’s Dangerous:
JWTs are stateless. You can’t “delete” them from the server if you don’t track them somewhere. Once issued, they remain valid until they expire.
How to Fix It:
- Maintain a token revocation list (in Redis, DB, etc.) for high-value apps.
- Consider using blacklisting or whitelisting strategies.
- Or use traditional session-based auth if you need full control.
Bonus: Best Practices for JWT Security
Here’s a quick summary of JWT security best practices you should follow:
→ Use strong, long secrets or key pairs
→ Always validate the JWT signature
→ Use HTTP-only, secure cookies (not localStorage)
→ Set short expiry times
→ Rotate refresh tokens
→ Validate alg
, aud
, and iss
claims
→ Never store sensitive data in JWT payloads
→ Use encryption only when necessary
→ Sanitize all inputs to prevent XSS
→ Prefer libraries with active maintenance and updates
Final Thoughts
JWTs can be powerful, scalable, and efficient—but only when used responsibly. Most security vulnerabilities in JWT implementations are the result of misconfiguration, shortcuts, or simply not understanding how tokens work under the hood.
As developers, we’re responsible for making authentication secure—not just functional.
You may also like:
Read more blogs from Here
Share your experiences in the comments, and let's discuss how to tackle them!
Follow me on LinkedIn
Top comments (0)