I'm going to be straight with you. Most JWT implementations I've looked at have at least one serious security hole. Not because developers are lazy, but because there are a bunch of traps that are super easy to fall into.
I've personally debugged JWT issues in production that led to exposed admin accounts, stolen user data, and one really embarrassing security disclosure. So yeah, I've learned these lessons the hard way. Let me save you from making the same mistakes.
Mistake #1: Using the "none" Algorithm
This is the dumbest vulnerability in JWT, and somehow it still happens. The JWT spec allows an algorithm called "none" which means "don't verify the signature at all." Yes, really.
Here's how hackers use it. They grab a valid JWT from your app, decode it, change whatever they want (maybe change their user ID to the admin's ID), set the algorithm to "none", and send it back. If your server isn't checking properly, it'll just accept the token.
What a bad token looks like:
{"alg":"none","typ":"JWT"}.{"userId":"admin","role":"superuser"}.{no signature}
And boom—they're now logged in as the admin. This has actually happened to major companies. It's not theoretical.
How to fix it: Make sure your JWT library is set to reject the "none" algorithm. Most good libraries do this by default now, but always check. In your verification code, explicitly specify which algorithms you accept.
// Good - explicitly specify allowed algorithms
jwt.verify(token, secret, { algorithms: ['HS256'] })
// Bad - accepts any algorithm including 'none'
jwt.verify(token, secret)
Mistake #2: Storing Sensitive Data in the Token
Remember how I said JWTs are signed but not encrypted? That means anyone can read what's inside. Just copy the token, paste it into any Base64 decoder, and see everything.
So why do people keep putting passwords, credit card numbers, and social security numbers in JWTs? I have no idea, but it happens way more often than you'd think.
Bad examples of what NOT to put in a JWT:
- User passwords (even hashed ones)
- Credit card numbers or payment details
- Social security numbers or tax IDs
- Private API keys
- Personal medical information
What you SHOULD put in a JWT:
- User ID or username
- Email address (if it's not super sensitive)
- User roles or permissions
- Token expiration time
- Token issued time
Think of JWT like a postcard, not a sealed letter. Only write stuff on it that you'd be okay with anyone reading.
Mistake #3: Not Setting Expiration Times (Or Setting Them Too Long)
Tokens that never expire are a hacker's dream. If someone steals a token—maybe from an old laptop, a browser that got hacked, or a network they intercepted—they can use it forever.
But I also see people who set crazy long expiration times. Like 30 days or even a year. That's almost as bad as no expiration at all.
What happens when tokens don't expire:
- Stolen tokens work forever, even after you change passwords
- You can't revoke access without rebuilding your entire system
- Former employees can still access your systems
- Compromised tokens stay valid indefinitely
Here's what I recommend: Set access tokens to expire in 15-60 minutes. Then use refresh tokens (with longer expiration) to get new access tokens when needed. Yeah, it's more work to implement, but it's way more secure.
// Good - short-lived token
const token = jwt.sign(
{ userId: user.id },
secret,
{ expiresIn: '15m' }
)
// Bad - token valid for a month
const token = jwt.sign(
{ userId: user.id },
secret,
{ expiresIn: '30d' }
)
⚠️ Real Story: A company I worked with had JWTs that never expired. When an employee got fired, they could still access the admin panel for months because their old token kept working. Don't be that company.
Mistake #4: Using Weak Secret Keys
Your secret key is the only thing protecting your JWTs from being forged. If someone gets your key or can guess it, they can create fake tokens that look completely real.
And yet, I've seen production apps using "secret", "password", the company name, or other garbage as their JWT secret key. One app I audited was using "jwt_secret" as the actual secret. Like, that's literally in every tutorial as the placeholder you're supposed to replace.
Weak keys I've actually seen in production:
- "secret" or "password"
- The company or app name
- "jwt_secret" or "my_secret_key"
- Short random strings like "abc123xyz"
- Dictionary words or phrases
Hackers run automated tools that try thousands of common secret keys. If yours is weak, they'll crack it in minutes, maybe seconds.
How to generate a proper secret key: Use at least 256 bits of cryptographically random data. That's 32 bytes, or a 64-character hex string. Don't make it up yourself—use a proper generator.
// Generate a secure key
openssl rand -hex 32
// Or in Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Once you have a good key, store it in environment variables or a secret manager. Never commit it to Git. Never email it. Treat it like a password to your entire system—because that's basically what it is.
Mistake #5: Not Validating Token Claims Properly
Just because a token has a valid signature doesn't mean you should trust everything in it. You need to check the actual data inside and make sure it makes sense.
Here's a real attack I've seen: someone grabbed their JWT, decoded it, changed their role from "user" to "admin", then re-encoded it. The signature didn't match anymore, but the developer was only checking if a token existed, not if it was actually valid.
Things you MUST validate:
- Signature: Obviously. Always verify the signature matches.
- Expiration: Check if the token is expired (the "exp" claim).
- Issued time: Make sure the token wasn't issued in the future (the "iat" claim).
- Issuer: Verify the token came from your system (the "iss" claim).
- Audience: Check if the token is meant for your app (the "aud" claim).
Don't just check if jwt.verify() returns something. Actually look at what's inside and make sure it's legit.
// Good - validate everything
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256'],
issuer: 'your-app',
audience: 'your-api'
})
if (decoded.exp < Date.now() / 1000) {
throw new Error('Token expired')
}
// Bad - only checking if token exists
if (req.headers.authorization) {
const token = req.headers.authorization
// Trusting the token without verification
}
Bonus Mistake: Storing Tokens in localStorage Without Thinking About XSS
Okay, this is a bit more complicated, but it's important. A lot of tutorials tell you to store your JWT in localStorage. And yeah, that works, but it has a problem: if your site has any XSS vulnerability, attackers can steal tokens straight from localStorage.
XSS (Cross-Site Scripting) means someone found a way to run their JavaScript on your site. Once they can do that, reading from localStorage is trivial. Then they've got your users' tokens.
Here are your options:
| Storage Method | XSS Risk | CSRF Risk | Best For |
|---|---|---|---|
| localStorage | High | Low | SPAs with good XSS protection |
| sessionStorage | High | Low | Same as localStorage |
| HttpOnly Cookie | Low | Medium | Traditional web apps |
| Memory (JS variable) | Medium | Low | Extra paranoid setups |
My take? If you're building a SPA, localStorage is probably fine as long as you're careful about XSS. Use a good framework (React, Vue, Angular), don't use dangerouslySetInnerHTML or v-html with user input, and sanitize everything.
If you want extra security, use HttpOnly cookies. They can't be accessed by JavaScript at all, so XSS attacks can't steal them. But then you need to worry about CSRF protection, which is a whole other thing.
How to Check If You're Making These Mistakes
Here's a quick checklist. Go through your JWT implementation and make sure you can answer "yes" to all of these:
- ✅ Do you explicitly reject the "none" algorithm?
- ✅ Are you checking the signature on every request?
- ✅ Is your secret key at least 256 bits of random data?
- ✅ Is your secret key stored securely (not in your code)?
- ✅ Do your tokens expire in less than an hour?
- ✅ Are you validating all the important claims (exp, iat, iss)?
- ✅ Are you avoiding storing sensitive data in tokens?
- ✅ Do you have a plan for handling XSS if you use localStorage?
If you answered "no" or "I don't know" to any of these, go fix it now. Seriously. These aren't nice-to-haves—they're the basics that keep your users safe.
The Bottom Line
JWT security isn't complicated, but it's easy to mess up if you're not paying attention. The mistakes I listed here have all caused real security problems in real apps with real users.
Don't use weak keys. Don't skip validation. Don't store sensitive data in tokens. Set expiration times. These are basic steps, but they make all the difference between a secure app and a data breach waiting to happen.
Take 30 minutes to go through your code and make sure you're not making these mistakes. Your future self (and your users) will thank you.