The Truth About JWT: When Not to Use JSON Web Tokens
JWTs are popular but often misused. Discover when to use them, when to avoid them, and how to implement proper session management in your applications.
While JWTs have become incredibly popular for authentication, they're often misused. Here's what every developer should know before implementing JWT authentication.
The Common Misconception
Many tutorials suggest using JWTs like this:
// DON'T DO THIS
const token = jwt.sign(
{ userId: user.id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '30d' }
);
This seems simple, but it creates several serious problems.
The Problems with JWT Sessions
1. Size Issues
// Traditional session cookie
sessionId: "abc123" // ~6 bytes
// vs JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywicm9sZSI6ImFkbWluIiw
iaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
// ~800 bytes or more
2. Can't Invalidate Individual Tokens
// With sessions
await db.sessions.delete({ userId: 123 });
// User immediately logged out
// With JWTs
// Token remains valid until expiration
// Can't revoke without complex blacklisting
3. Security Vulnerabilities
// Common mistake: storing sensitive data
const token = jwt.sign({
userId: user.id,
email: user.email, // Don't do this
creditCard: user.ccLast4, // Really don't do this
role: user.role
}, secret);
When to Actually Use JWTs
1. Microservices Authentication
// Service A generates token for Service B
const serviceToken = jwt.sign(
{
service: 'payment-processor',
permissions: ['process-payment'],
exp: Math.floor(Date.now() / 1000) + (60 * 60) // 1 hour
},
SERVICE_SECRET
);
2. One-Time Operations
// Password reset token
const resetToken = jwt.sign(
{
userId: user.id,
purpose: 'password-reset',
exp: Math.floor(Date.now() / 1000) + (15 * 60) // 15 minutes
},
RESET_SECRET
);
Better Session Management
1. Traditional Sessions
// Express session example
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true,
httpOnly: true,
maxAge: 1000 * 60 * 60 * 24 // 24 hours
}
}));
2. Redis Session Store
const RedisStore = require('connect-redis')(session);
const redis = require('redis');
const client = redis.createClient();
app.use(session({
store: new RedisStore({ client }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false
}));
Real World Performance Impact
Memory Usage
1000 Active Users:
Sessions with Redis:
- Session IDs: ~6KB
- Fast lookups
- Easy to scale
JWT Solution:
- Tokens: ~800KB
- Sent with every request
- Increased bandwidth usage
Database Load
Sessions:
- One query per session validation
- Can cache in Redis
JWT:
- Must maintain blacklist for logout
- Growing blacklist size
- Complex invalidation logic
Security Best Practices
1. If You Must Use JWTs
// Minimal payload
const token = jwt.sign(
{ sub: user.id },
process.env.JWT_SECRET,
{
expiresIn: '1h',
algorithm: 'HS256'
}
);
// Separate user data fetch
const userData = await db.users.findById(decoded.sub);
2. Better Session Security
// Session configuration
app.use(session({
secret: process.env.SESSION_SECRET,
name: '__Host-sess', // Custom cookie name
cookie: {
secure: true,
httpOnly: true,
sameSite: 'strict',
maxAge: 1000 * 60 * 60 * 24
}
}));
Migration Strategy
Moving from JWT to Sessions
// 1. Add session support while maintaining JWT
app.use(session(...));
app.use(jwtAuth); // Existing JWT middleware
// 2. Create sessions for JWT users
app.use(async (req, res, next) => {
if (req.user && !req.session.userId) {
req.session.userId = req.user.id;
}
next();
});
// 3. Gradually phase out JWT
Conclusion
JWTs are great for:
- Service-to-service authentication
- One-time tokens
- Stateless microservices
But problematic for:
- User sessions
- Stateful applications
- Systems requiring token revocation
Consider traditional sessions with Redis for better:
- Security
- Performance
- Flexibility
- Memory usage