Skip to main content

Common Misconfiguration

Weak or exposed JWT secrets allow attackers to forge tokens, bypass authentication, and gain unauthorized access to systems.

Vulnerable Example

// VULNERABLE - Hardcoded JWT secrets
const jwt = require('jsonwebtoken');

// Never hardcode JWT secrets!
const JWT_SECRET = 'my-super-secret-key'; // Weak secret
const REFRESH_SECRET = 'refresh-secret-123'; // Predictable

// Using HS256 with weak secret
function generateToken(userId) {
    return jwt.sign(
        { userId, role: 'admin' },
        JWT_SECRET,
        { expiresIn: '7d' } // Long expiration
    );
}

// No token validation
function verifyToken(token) {
    try {
        return jwt.verify(token, JWT_SECRET);
    } catch (err) {
        return null; // Silently fail
    }
}

// Storing sensitive data in JWT
function createInsecureToken(user) {
    return jwt.sign({
        id: user.id,
        email: user.email,
        password: user.password, // Never include passwords!
        creditCard: user.creditCard, // Never include sensitive data!
        ssn: user.ssn
    }, JWT_SECRET);
}
# VULNERABLE - Flask JWT implementation
from flask import Flask
from flask_jwt_extended import JWTManager, create_access_token
import datetime

app = Flask(__name__)

# Weak and hardcoded secrets
app.config['JWT_SECRET_KEY'] = 'super-secret' # Never hardcode!
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = datetime.timedelta(days=365) # Too long!
app.config['JWT_ALGORITHM'] = 'HS256' # Consider RS256 for better security

jwt = JWTManager(app)

# No refresh token rotation
@app.route('/login', methods=['POST'])
def login():
    # ... authentication logic ...
    access_token = create_access_token(
        identity=user_id,
        additional_claims={'role': 'admin', 'permissions': ['all']} # Too much info
    )
    return {'token': access_token}

# Using none algorithm (dangerous!)
def decode_token_unsafe(token):
    import jwt
    return jwt.decode(token, options={"verify_signature": False}) # Never do this!

Secure Example

// SECURE - Proper JWT implementation
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const fs = require('fs').promises;

class SecureJWTManager {
    constructor() {
        this.accessTokenSecret = null;  // For HS256 in dev
        this.refreshTokenSecret = null; // For HS256 in dev
        this.publicKey = null;
        this.privateKey = null;
        this.isAsymmetric = false;
        this.initialized = false;
    }

    async initialize() {
        // Use RS256 with key pairs for production
        if (process.env.NODE_ENV === 'production') {
            await this.loadRSAKeys();
            this.isAsymmetric = true;
        } else {
            // Use strong random secrets for development
            this.loadHMACSecrets();
            this.isAsymmetric = false;
        }
        
        this.initialized = true;
    }

    async loadRSAKeys() {
        // Load RSA key pair from secure location
        try {
            this.privateKey = await fs.readFile(process.env.JWT_PRIVATE_KEY_PATH, 'utf8');
            this.publicKey = await fs.readFile(process.env.JWT_PUBLIC_KEY_PATH, 'utf8');
        } catch (error) {
            console.error('Failed to load RSA keys:', error);
            throw new Error('JWT key configuration failed');
        }
    }

    loadHMACSecrets() {
        // Load from environment with validation
        const accessSecret = process.env.JWT_ACCESS_SECRET;
        const refreshSecret = process.env.JWT_REFRESH_SECRET;
        
        if (!accessSecret || accessSecret.length < 32) {
            throw new Error('JWT_ACCESS_SECRET must be at least 32 characters');
        }
        
        if (!refreshSecret || refreshSecret.length < 32) {
            throw new Error('JWT_REFRESH_SECRET must be at least 32 characters');
        }
        
        this.accessTokenSecret = accessSecret;
        this.refreshTokenSecret = refreshSecret;
    }

    generateTokenPair(userId, userInfo = {}) {
        // Minimal payload - don't include sensitive data
        const iat = Math.floor(Date.now() / 1000);
        const accessPayload = {
            sub: userId,
            type: 'access',
            iat: iat
        };
        
        const algorithm = this.isAsymmetric ? 'RS256' : 'HS256';
        
        // Access token - short lived
        const accessToken = this.signToken(accessPayload, {
            algorithm: algorithm,
            expiresIn: '15m', // Short expiration
            issuer: 'api.example.com',
            audience: 'app.example.com'
        });
        
        // Refresh token - longer lived but still limited
        const refreshPayload = {
            sub: userId,
            type: 'refresh',
            iat: iat,
            family: crypto.randomBytes(16).toString('hex') // Token family for rotation
        };
        
        const refreshToken = this.signToken(refreshPayload, {
            algorithm: algorithm, // Use the same strong algorithm
            expiresIn: '7d',
            issuer: 'api.example.com'
        });
        
        return {
            accessToken,
            refreshToken,
            expiresIn: 900 // 15 minutes in seconds
        };
    }

    signToken(payload, options) {
        let secret;
        if (this.isAsymmetric) {
            secret = this.privateKey;
        } else {
            secret = (payload.type === 'refresh' ? this.refreshTokenSecret : this.accessTokenSecret);
        }
        
        return jwt.sign(payload, secret, options);
    }

    verifyToken(token, isRefreshToken = false) {
        try {
            let secret, algorithms;
            
            if (this.isAsymmetric) {
                secret = this.publicKey;
                algorithms = ['RS256'];
            } else {
                secret = isRefreshToken ? this.refreshTokenSecret : this.accessTokenSecret;
                algorithms = ['HS256'];
            }
            
            const decoded = jwt.verify(token, secret, {
                algorithms: algorithms,
                issuer: 'api.example.com',
                audience: isRefreshToken ? undefined : 'app.example.com', // Audience may not apply to refresh
                clockTolerance: 30 // 30 seconds clock skew tolerance
            });
            
            // Additional validation
            const expectedType = isRefreshToken ? 'refresh' : 'access';
            if (decoded.type !== expectedType) {
                throw new Error('Invalid token type');
            }
            
            return decoded;
        } catch (error) {
            // Log for security monitoring
            console.error('Token verification failed:', error.message);
            throw error;
        }
    }
    
    async verifyRefreshTokenAndRotate(token) {
        try {
            const decoded = this.verifyToken(token, true);
            
            // Check if token family is valid (for rotation detection)
            // const isValidFamily = await this.checkTokenFamily(decoded.family);
            // if (!isValidFamily) {
            //     // Possible token reuse - revoke all tokens for this user
            //     await this.revokeUserTokens(decoded.sub);
            //     throw new Error('Token reuse detected');
            // }
            
            // Invalidate old token
            // await this.invalidateToken(token);

            // Generate new token pair
            return this.generateTokenPair(decoded.sub);
            
        } catch (error) {
            console.error('Refresh token verification failed:', error.message);
            throw error;
        }
    }

    // Generate cryptographically secure secret
    static generateSecret(length = 64) {
        return crypto.randomBytes(length).toString('base64');
    }
}

// Express middleware for JWT validation
const jwtMiddleware = (jwtManager) => {
    return async (req, res, next) => {
        const authHeader = req.headers.authorization;
        
        if (!authHeader || !authHeader.startsWith('Bearer ')) {
            return res.status(401).json({ error: 'No token provided' });
        }
        
        const token = authHeader.substring(7);
        
        try {
            const decoded = jwtManager.verifyToken(token, false);
            req.user = decoded;
            
            // Check if token is close to expiration
            const now = Math.floor(Date.now() / 1000);
            if (decoded.exp - now < 300) { // Less than 5 minutes
                res.setHeader('X-Token-Expiring-Soon', 'true');
            }
            
            next();
        } catch (error) {
            return res.status(401).json({ error: 'Invalid token' });
        }
    };
};
# SECURE - Kubernetes secret for JWT keys
apiVersion: v1
kind: Secret
metadata:
  name: jwt-keys
  namespace: production
type: Opaque
data:
  jwt-private-key: LS0tLS1CRUdJTi... # Base64 encoded RSA private key
  jwt-public-key: LS0tLS1CRUdJTi...  # Base64 encoded RSA public key
  # Fallback HMAC secrets (if needed, otherwise remove)
  jwt-access-secret: <base64-encoded-random-secret>
  jwt-refresh-secret: <base64-encoded-random-secret>
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: jwt-config
data:
  JWT_ALGORITHM: "RS256"
  JWT_ISSUER: "api.example.com"
  JWT_AUDIENCE: "app.example.com"
  JWT_ACCESS_TOKEN_EXPIRES: "15m"
  JWT_REFRESH_TOKEN_EXPIRES: "7d"
# Generate RSA keys for JWT
# Generate private key
openssl genpkey -algorithm RSA -out jwt-private.pem -pkeyopt rsa_keygen_bits:4096

# Generate public key
openssl rsa -pubout -in jwt-private.pem -out jwt-public.pem

# Generate random secrets for dev (HS256)
openssl rand -base64 64 > jwt-access-secret.txt
openssl rand -base64 64 > jwt-refresh-secret.txt

Detection Patterns

  • JWT Token Format: `eyJ[A-Za-z0-9-_]+\.eyJ[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+`
  • Algorithm “none” Vulnerability: `("alg"|"algorithm")\s*:\s*("none"|"None")`
  • Common Weak Secret: `(secret|password)['"]?\s*[:=]\s*['"](secret|12345|admin|jwt|super-secret)['"]`
  • Hardcoded Secret Variable: `(JWT_SECRET|SECRET_KEY)\s*[:=]\s*['"][^'"]{1,20}['"]` (Finds short, likely weak secrets)

Prevention Best Practices

  1. Use Strong, Random Secrets: A weak, guessable secret (like ‘secret123’) makes your token forgeable. Use a cryptographically secure random string (at least 256 bits / 32 characters) and load it from a secrets manager or environment variable.
  2. Prefer Asymmetric Algorithms (RS256): HS256 (symmetric) uses one secret to sign and verify. RS256 (asymmetric) uses a private key to sign and a public key to verify. This is safer because you can share the public key with other services for verification without exposing the private signing key.
  3. Short Access Token Expiration: Access tokens (which grant access) should be very short-lived (e.g., 5-15 minutes). This dramatically limits the window of opportunity if a token is stolen.
  4. Implement Token Refresh: Use a separate, long-lived “refresh token” (e.g., 7 days) to get a new access token. This refresh token should be stored securely (as an httpOnly cookie) and ideally implement rotation (where using a refresh token invalidates it and issues a new one).
  5. Minimal, Non-Sensitive Payload: A JWT is signed (tamper-proof) but not encrypted (it’s Base64 encoded, which is reversible). Anyone can read its contents. Never put sensitive data like passwords, permissions, or PII in the payload. Use the sub (subject) claim to store the user ID and nothing more.
  6. Validate All Claims: On the server, always verify the signature. Also, verify the exp (expiration), iss (issuer), and aud (audience) claims to ensure the token is not expired and was intended for your specific service.
  7. Implement Token Revocation: JWTs are stateless, which means they are valid until they expire. For critical events (like a user logging out or changing their password), you need a way to “revoke” their token. This usually involves maintaining a “denylist” (e.g., in Redis) of token IDs that are no longer valid.
  8. Secure Client-Side Storage: Do not store JWTs in localStorage on the client, as it’s vulnerable to XSS attacks. Store them in secure, httpOnly cookies, which cannot be accessed by JavaScript.
  9. Monitor for Token Anomalies: Log and alert on token verification failures, attempts to use expired tokens, or impossible-to-achieve token refreshes. This can indicate an attacker is trying to forge or replay tokens.
  10. Rotate Your Keys: The secrets and private keys used to sign tokens should be rotated regularly (e.g., every 90 days). This limits the lifespan of any key that might have been leaked.