The Truth About JWT: When to Use It, When to Avoid It
Think JWT is always the answer? Learn about its security implications, best practices, and when traditional sessions might be a better choice.
JWT (JSON Web Tokens) isn't always the best authentication solution. Here's what nobody tells you about its security implications and alternatives.
Common Misconceptions
1. "JWTs are Always Secure"
// ❌ BAD: Unsafe JWT implementation
const createToken = (userId: string) => {
return jwt.sign(
{ userId },
'secret123', // Hardcoded secret
{ expiresIn: '1y' } // Too long expiration
);
};
// ✅ BETTER: Secure JWT implementation
const createToken = (userId: string) => {
return jwt.sign(
{
userId,
sessionId: crypto.randomUUID(),
issuedAt: Date.now()
},
process.env.JWT_SECRET,
{
expiresIn: '15m',
algorithm: 'HS256'
}
);
};
2. "JWTs Replace Sessions"
// ❌ BAD: Storing sensitive data in JWT
const token = jwt.sign({
userId: user.id,
role: user.role,
permissions: user.permissions,
email: user.email,
// Don't store sensitive data in tokens!
}, secret);
// ✅ BETTER: Minimal JWT with session reference
const token = jwt.sign({
sub: user.id,
jti: sessionId, // Session reference
iat: Date.now()
}, secret);
Security Concerns
1. Token Invalidation
// ❌ BAD: No way to invalidate tokens
app.post('/logout', (req, res) => {
// Client deletes token, but it's still valid!
res.clearCookie('token');
res.json({ message: 'Logged out' });
});
// ✅ BETTER: Token blacklisting with Redis
class TokenBlacklist {
constructor(private redis: Redis) {}
async invalidate(token: string) {
const decoded = jwt.decode(token) as JwtPayload;
const timeLeft = decoded.exp! - Date.now() / 1000;
await this.redis.setex(
`blacklist:${token}`,
Math.ceil(timeLeft),
'1'
);
}
async isBlacklisted(token: string): Promise<boolean> {
return await this.redis.exists(`blacklist:${token}`) === 1;
}
}
2. XSS Protection
// ❌ BAD: Vulnerable to XSS
app.use(cookieParser());
app.get('/api/auth', (req, res) => {
res.cookie('token', token); // No security options
});
// ✅ BETTER: Secure cookie settings
app.get('/api/auth', (req, res) => {
res.cookie('token', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/api',
maxAge: 900000 // 15 minutes
});
});
Better Patterns
1. Refresh Token Flow
class AuthService {
async createTokenPair(userId: string) {
const accessToken = jwt.sign(
{ sub: userId },
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = crypto.randomBytes(40).toString('hex');
await this.redis.setex(
`refresh:${refreshToken}`,
30 * 24 * 60 * 60, // 30 days
userId
);
return { accessToken, refreshToken };
}
async refresh(refreshToken: string) {
const userId = await this.redis.get(`refresh:${refreshToken}`);
if (!userId) throw new Error('Invalid refresh token');
// Rotate refresh token
await this.redis.del(`refresh:${refreshToken}`);
return this.createTokenPair(userId);
}
}
2. Session-JWT Hybrid
class HybridAuth {
async createSession(user: User) {
const sessionId = crypto.randomUUID();
// Store session data
await this.redis.hset(`session:${sessionId}`, {
userId: user.id,
role: user.role,
createdAt: Date.now()
});
// Create JWT with session reference
const token = jwt.sign(
{
sub: user.id,
jti: sessionId
},
process.env.JWT_SECRET,
{ expiresIn: '15m' }
);
return token;
}
async validateToken(token: string) {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const session = await this.redis.hgetall(
`session:${decoded.jti}`
);
if (!session) throw new Error('Invalid session');
return session;
}
}
Performance Considerations
1. Token Size
// ❌ BAD: Large tokens
const bloatedToken = jwt.sign({
user: {
id: 1,
name: 'John',
email: 'john@example.com',
permissions: ['read', 'write', 'admin'],
preferences: {
theme: 'dark',
language: 'en',
notifications: true
},
metadata: {
lastLogin: '2024-01-20T00:00:00Z',
loginCount: 42
}
}
}, secret);
// ✅ BETTER: Minimal tokens
const efficientToken = jwt.sign({
sub: '1',
jti: 'session_id'
}, secret);
2. Caching Strategy
class TokenValidator {
private cache: LRUCache<string, SessionData>;
constructor() {
this.cache = new LRUCache({
max: 10000,
maxAge: 15 * 60 * 1000 // 15 minutes
});
}
async validateToken(token: string) {
// Check cache first
const cached = this.cache.get(token);
if (cached) return cached;
// Validate and decode token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Get session data
const session = await this.getSession(decoded.jti);
// Cache result
this.cache.set(token, session);
return session;
}
}
When to Use JWT
1. Good Use Cases
// ✅ GOOD: API-only services
class ApiAuthService {
async createServiceToken(serviceId: string) {
return jwt.sign(
{
sub: serviceId,
type: 'service',
scope: ['api:read']
},
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
}
}
// ✅ GOOD: Stateless microservices
class MicroserviceAuth {
validateServiceToken(token: string) {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
return decoded.type === 'service';
}
}
2. Bad Use Cases
// ❌ BAD: User sessions with sensitive data
class UserSession {
// Don't store sensitive data in JWT
createUserToken(user: User) {
return jwt.sign({
sub: user.id,
email: user.email,
creditCard: user.paymentInfo,
address: user.address
}, secret);
}
}
// ❌ BAD: Long-lived sessions
class PersistentAuth {
// Don't use long-lived JWTs
createPermanentToken(userId: string) {
return jwt.sign(
{ sub: userId },
secret,
{ expiresIn: '1y' }
);
}
}
Alternatives
1. Session-Based Auth
class SessionAuth {
async createSession(user: User) {
const sessionId = crypto.randomUUID();
await this.redis.hset(`session:${sessionId}`, {
userId: user.id,
createdAt: Date.now(),
userAgent: user.userAgent
});
return sessionId;
}
async validateSession(sessionId: string) {
const session = await this.redis.hgetall(
`session:${sessionId}`
);
if (!session) throw new Error('Invalid session');
return session;
}
}
2. API Keys
class ApiKeyAuth {
async createApiKey(userId: string) {
const apiKey = `key_${crypto.randomBytes(32).toString('hex')}`;
const hashedKey = await bcrypt.hash(apiKey, 10);
await this.db.apiKeys.create({
userId,
hashedKey,
createdAt: new Date()
});
return apiKey;
}
async validateApiKey(apiKey: string) {
const hashedKey = await bcrypt.hash(apiKey, 10);
return await this.db.apiKeys.findOne({
where: { hashedKey }
});
}
}
Conclusion
Use JWT when:
- Building stateless APIs
- Short-lived tokens needed
- Service-to-service auth
- Limited session data
Avoid JWT when:
- Managing user sessions
- Storing sensitive data
- Need immediate invalidation
- Long-lived tokens required
Remember: JWT is a tool, not a solution for every auth problem.