JSON Web Tokens (JWTs) are the backbone of modern stateless authentication. But their flexibility comes with risk — misconfigured JWTs are a top source of authentication bypasses and data leaks.

How JWTs Work

A JWT consists of three Base64URL-encoded parts separated by dots:

// Header: algorithm and token type
{ "alg": "RS256", "typ": "JWT" }

// Payload: claims (the actual data)
{ "sub": "user123", "exp": 1745635200, "role": "admin" }

// Signature: cryptographic proof of integrity
HMACSHA256(base64Url(header) + "." + base64Url(payload), secret)

Common Vulnerabilities

1. Algorithm Confusion Attack

Attackers change the header to "alg": "none" or switch from RS256 to HS256:

// Attack: Change algorithm to "none"
{ "alg": "none", "typ": "JWT" }.
{ "sub": "admin", "role": "superuser" }.
// Empty signature — server accepts it!

Fix: Always explicitly whitelist allowed algorithms on the server.

2. Weak Signing Secrets

Using short or predictable secrets enables brute-force attacks:

// BAD: Weak secrets
const secret = "mySecret";          // Too short
const secret = "password123";       // Dictionary word

// GOOD: Strong secrets
const secret = crypto.randomBytes(64).toString('hex');
// Or use asymmetric keys (RS256) for better security

3. Token Expiration Issues

  • Missing exp claim → tokens valid forever
  • Setting exp in the past → broken auth
  • Very long expiration → larger attack window

Security Best Practices

Token Storage

MethodXSS RiskCSRF RiskRecommendation
localStorageHighLowNot recommended
Cookie (no HttpOnly)HighHighNever use
Cookie (HttpOnly + Secure)LowMediumRecommended
In-memory + refresh tokenLowLowBest for SPAs

Implementation Checklist

  • ✅ Use RS256 (asymmetric) for distributed systems, HS256 for monoliths
  • ✅ Set short access token expiration (15-30 minutes)
  • ✅ Use refresh tokens with rotation for long-lived sessions
  • ✅ Validate all claims: iss, aud, exp, nbf
  • ✅ Include a unique jti (JWT ID) for token revocation
  • ✅ Store tokens in HttpOnly, Secure, SameSite cookies
  • ✅ Implement token blocklist for logout and revocation

Secure Implementation Example

import jwt from 'jsonwebtoken';

// Verify token with strict validation
function verifyToken(token) {
    return jwt.verify(token, publicKey, {
        algorithms: ['RS256'],           // Whitelist algorithms
        issuer: 'https://api.myapp.com',
        audience: 'myapp-frontend',
        clockTolerance: 5,               // 5 second leeway
        maxAge: '30m'                    // Max age even if exp allows
    });
}

// Generate secure access token
function generateAccessToken(user) {
    return jwt.sign(
        {
            sub: user.id,
            role: user.role,
            jti: crypto.randomUUID()     // Unique token ID
        },
        privateKey,
        {
            algorithm: 'RS256',
            expiresIn: '15m',
            issuer: 'https://api.myapp.com',
            audience: 'myapp-frontend'
        }
    );
}

Refresh Token Pattern

// Access token: short-lived (15 min), stored in memory
// Refresh token: long-lived (7 days), stored in HttpOnly cookie

app.post('/auth/refresh', (req, res) => {
    const refreshToken = req.cookies.refresh_token;
    
    // Verify refresh token
    const payload = verifyRefreshToken(refreshToken);
    
    // Rotate: invalidate old, issue new
    revokeToken(refreshToken);
    const newAccess = generateAccessToken(payload.user);
    const newRefresh = generateRefreshToken(payload.user);
    
    res.cookie('refresh_token', newRefresh, {
        httpOnly: true,
        secure: true,
        sameSite: 'strict',
        maxAge: 7 * 24 * 60 * 60 * 1000
    });
    
    res.json({ access_token: newAccess });
});

Key Takeaways

  • Always whitelist allowed algorithms — never trust the header
  • Use strong signing secrets (256-bit minimum for HS256)
  • Keep access tokens short-lived and use refresh token rotation
  • Store tokens in HttpOnly + Secure cookies, not localStorage
  • Validate all standard claims on every request