Back

JWT Security Best Practices: Protect Your Authentication System

JWT Security Best Practices: Protect Your Authentication System

JSON Web Tokens are widely used for authentication, but improper implementation can lead to serious security vulnerabilities. This guide covers essential security practices to protect your JWT-based authentication system.

Why JWT Security Matters

JWT tokens are self-contained credentials. Anyone who possesses a valid token can access protected resources until the token expires. Unlike session-based authentication, there's no central revocation mechanism—making security practices critical.

Token Storage Security

The Storage Dilemma

Storage Method XSS Risk CSRF Risk Recommendation
localStorage High None ❌ Avoid
sessionStorage High None ❌ Avoid
HttpOnly Cookie None High ✅ Use with CSRF protection
Memory None None ✅ Best for sensitive apps

Recommended Approach

For Web Applications: Use HttpOnly cookies with SameSite attribute:

Set-Cookie: token=eyJhbGci...; HttpOnly; Secure; SameSite=Strict; Path=/

For Mobile/SPA: Store in memory, refresh on app load:

// Store in memory (not localStorage)
let accessToken = null;

function setToken(token) {
  accessToken = token;
}

function getToken() {
  return accessToken;
}

Signing Algorithm Security

Algorithm Selection

Algorithm Security Level When to Use
RS256 High Public APIs, third-party access (asymmetric)
ES256 High Modern apps, mobile, IoT (smaller signatures)
HS256 High (with strong secret) Internal services only (symmetric)

Algorithm Confusion Attack Prevention

Vulnerability: Attackers can change the algorithm to "none" or switch from RS256 to HS256.

Prevention: Always specify allowed algorithms:

// ❌ Vulnerable
const decoded = jwt.verify(token, publicKey);

// ✅ Secure
const decoded = jwt.verify(token, publicKey, {
  algorithms: ['RS256']
});

Key Management

// ❌ Bad: Hardcoded secret
const secret = 'my-secret-key';

// ✅ Good: Environment variable
const secret = process.env.JWT_SECRET;

// ✅ Better: Key rotation support
const keyStore = new KeyStore();
const key = await keyStore.getCurrentKey();

Token Lifetime Management

Access Token Expiration

Token Type Recommended Lifetime Reason
Access Token 15-60 minutes Limit exposure window
Refresh Token 7-30 days Balance UX and security
Password Reset 5-15 minutes Single-use, time-sensitive

Implementing Token Refresh

// Token refresh with rotation
async function refreshTokens(refreshToken) {
  // 1. Verify refresh token
  const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
  
  // 2. Check if token is blacklisted
  if (await isTokenBlacklisted(refreshToken)) {
    throw new Error('Token revoked');
  }
  
  // 3. Generate new tokens
  const newAccessToken = generateAccessToken(decoded.userId);
  const newRefreshToken = generateRefreshToken(decoded.userId);
  
  // 4. Invalidate old refresh token
  await blacklistToken(refreshToken);
  
  return { accessToken: newAccessToken, refreshToken: newRefreshToken };
}

Common JWT Vulnerabilities

1. None Algorithm Attack

Attack: Attacker sets alg: "none" to bypass signature verification. Some JWT libraries treat "none" as valid and skip verification.

Prevention:

// Always explicitly specify allowed algorithms
const decoded = jwt.verify(token, secret, {
  algorithms: ['HS256', 'RS256'] // Explicitly allow only these
});

// Never accept "none" algorithm
if (header.alg === 'none' || !header.alg) {
  throw new Error('Invalid algorithm: none is not allowed');
}

2. Weak Secret Attack

Attack: Brute force weak HMAC secrets.

Prevention:

// Use strong secrets (256+ bits)
const secret = crypto.randomBytes(64).toString('hex');

// For RS256, use proper key lengths
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
  modulusLength: 2048
});

3. Token Leakage via URL

Attack: Tokens logged in server logs when passed in URL.

Prevention:

// ❌ Bad: Token in URL
fetch(`/api/user?token=${token}`);

// ✅ Good: Token in header
fetch('/api/user', {
  headers: { 'Authorization': `Bearer ${token}` }
});

4. Missing Claim Validation

Attack: Attacker reuses token from different context.

Prevention:

function validateToken(token, expectedAudience, expectedIssuer) {
  const decoded = jwt.verify(token, secret, {
    audience: expectedAudience,
    issuer: expectedIssuer,
    clockTimestamp: Math.floor(Date.now() / 1000)
  });
  
  // Additional validation
  if (decoded.exp < Date.now() / 1000) {
    throw new Error('Token expired');
  }
  
  return decoded;
}

Token Revocation Strategies

Since JWT tokens are stateless, revocation requires additional infrastructure:

1. Token Blacklist

// Redis-based blacklist
async function revokeToken(token) {
  const decoded = jwt.decode(token);
  const ttl = decoded.exp - Math.floor(Date.now() / 1000);
  
  if (ttl > 0) {
    await redis.setex(`blacklist:${token}`, ttl, '1');
  }
}

async function isTokenRevoked(token) {
  return await redis.exists(`blacklist:${token}`);
}

2. Token Version

// Store token version in user record
const token = jwt.sign({ 
  userId, 
  version: user.tokenVersion 
}, secret);

// Verify version
const decoded = jwt.verify(token, secret);
if (decoded.version !== user.tokenVersion) {
  throw new Error('Token revoked');
}

// Revoke all tokens
await user.update({ tokenVersion: user.tokenVersion + 1 });

3. Short Lifetime + Refresh

The simplest approach: use very short-lived access tokens (5-15 minutes) with refresh tokens.

Security Headers for JWT

# Prevent token leakage via referrer
Referrer-Policy: no-referrer

# Prevent clickjacking
X-Frame-Options: DENY

# Prevent MIME sniffing
X-Content-Type-Options: nosniff

# Enable HSTS
Strict-Transport-Security: max-age=31536000; includeSubDomains

JWT Security Checklist

Implementation Checklist

  • Use HTTPS for all token transmission
  • Use strong secrets (256+ bits for HMAC, 2048+ bits for RSA)
  • Specify allowed algorithms explicitly
  • Validate all claims (iss, aud, exp, iat)
  • Set appropriate token expiration
  • Implement refresh token rotation
  • Store tokens securely (HttpOnly cookies or memory)
  • Never store sensitive data in JWT payload
  • Implement token revocation mechanism
  • Add rate limiting to authentication endpoints

Production Checklist

  • Rotate signing keys periodically
  • Monitor for suspicious token usage
  • Log authentication events
  • Implement account lockout after failed attempts
  • Use different secrets for different environments
  • Set up alerts for token-related errors

Testing Your JWT Security

Use our JWT Decoder to:

  1. Verify token structure and claims
  2. Check expiration times
  3. Inspect algorithm used
  4. Debug authentication issues

Conclusion

JWT security requires careful implementation. By following these best practices—proper storage, strong algorithms, claim validation, and revocation mechanisms—you can build a secure authentication system that protects your users and your application.

Key Security Principles:

  • Defense in depth: Multiple layers of protection
  • Least privilege: Minimal token permissions
  • Fail secure: Reject tokens on any validation failure
  • Audit everything: Log and monitor token usage

For quick JWT inspection and debugging, try our JWT Decoder tool.