JWT Security Pitfalls: What Your First Production API Gets Wrong
Most developers ship JWTs with critical flaws. We break down the five mistakes that compromise real production APIs and how to fix them.
You've built your API, added JWT authentication, and deployed to production. Your users can log in. Everything works. And then someone finds out they can tamper with their token and grant themselves admin access.
JWT security isn't complicated, but the gaps between "it works" and "it's secure" are where most first-time production APIs fail. Here are the mistakes we see constantly—and how to avoid them.
1. Trusting Unsigned Tokens or Weak Secrets
The most dangerous mistake: using a weak or hardcoded secret key, or worse, not verifying the signature at all.
Your secret must be:
- At least 256 bits of entropy (use a cryptographically secure random generator)
- Stored in environment variables, never in code
- Rotated periodically
typescript// ❌ DON'T DO THIS const secret = "mysecret123"; const token = jwt.sign(payload, secret); // ✅ DO THIS const secret = process.env.JWT_SECRET; // e.g., crypto.randomBytes(32).toString('hex') jwt.verify(token, secret, { algorithms: ['HS256'] });
Always explicitly specify the algorithm during verification. Never trust the
algnonetypescript// ❌ VULNERABLE jwt.verify(token, secret); // ✅ SECURE jwt.verify(token, secret, { algorithms: ['HS256'] });
2. Not Implementing Token Expiration
A stolen token is only valuable if it works forever. Without expiration, a compromised token becomes a permanent backdoor.
Set reasonable expiration times and enforce them:
pythonimport jwt from datetime import datetime, timedelta payload = { 'user_id': 123, 'exp': datetime.utcnow() + timedelta(hours=1), 'iat': datetime.utcnow() } token = jwt.encode(payload, secret, algorithm='HS256')
On verification, the library will automatically reject expired tokens. Pair short-lived access tokens (15 minutes) with refresh tokens to balance security and usability.
3. Storing Sensitive Data in the Payload
JWT tokens are encoded, not encrypted. Anyone can decode the payload and read its contents.
bash# Anyone can do this echo "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U" | cut -d'.' -f2 | base64 -d
Never store passwords, API keys, credit card numbers, or PII in tokens. Store only immutable identifiers:
typescript// ❌ DON'T DO THIS const payload = { user_id: user.id, email: user.email, role: user.role, password_hash: user.password_hash // NEVER }; // ✅ DO THIS const payload = { user_id: user.id, role: user.role };
4. Missing Token Revocation
You can't revoke a valid, unexpired JWT without infrastructure. If a user logs out or their account is compromised, their token still works.
Implement one of these approaches:
Blacklist (simple, scales okay): Store invalidated token IDs in Redis with a TTL matching the token expiration.
typescript// On logout await redis.setex(`token_blacklist:${tokenId}`, expirationTime, '1'); // On request const isBlacklisted = await redis.get(`token_blacklist:${tokenId}`); if (isBlacklisted) throw new UnauthorizedError();
Short expiration + refresh tokens (recommended): Keep access tokens short-lived (15 min). Refresh tokens are long-lived but can be revoked in a database. When a user logs out, revoke their refresh token.
5. Using the Wrong Algorithm
HS256 (HMAC with SHA-256) is symmetric: the same secret signs and verifies tokens. This works fine for monolithic APIs but creates key-sharing problems in distributed systems.
RS256 (RSA) is asymmetric: a private key signs, a public key verifies. Better for microservices and third-party integrations.
typescriptconst fs = require('fs'); const privateKey = fs.readFileSync('private.key', 'utf8'); const publicKey = fs.readFileSync('public.key', 'utf8'); // Sign with private key const token = jwt.sign(payload, privateKey, { algorithm: 'RS256' }); // Verify with public key jwt.verify(token, publicKey, { algorithms: ['RS256'] });
Choose based on your architecture, but be intentional about it.
The Bottom Line
JWT security is about three things: strong keys, short lifespans, and minimal payload. If you're building an API at LavaPi or anywhere else, start with these five fixes before pushing to production. Token compromise is silent and hard to detect—lock the door now.
LavaPi Team
Digital Engineering Company