JWT Secret Keys Exposed in Node.js Applications

Critical Risk Secrets Exposure
jwtnodejsauthenticationtoken-signingsecret-keysauthorizationsession-management

What it is

A critical security vulnerability where JSON Web Token (JWT) signing secrets are hardcoded in Node.js source code, configuration files, or accidentally exposed through logs and debugging information. Exposed JWT secrets allow attackers to forge valid tokens, impersonate users, escalate privileges, and completely bypass authentication mechanisms.

// VULNERABLE: Express.js app with exposed JWT secrets
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const app = express();

// VULNERABLE: Hardcoded JWT secrets
const JWT_SECRET = 'my-secret-key';
const REFRESH_SECRET = 'my-refresh-secret';
const ADMIN_SECRET = 'admin-jwt-secret-123';

app.use(express.json());

// VULNERABLE: Login endpoint with hardcoded secrets
app.post('/login', async (req, res) => {
    try {
        const { username, password } = req.body;
        
        // Mock user authentication
        const user = await User.findOne({ username });
        if (!user || !await bcrypt.compare(password, user.password)) {
            return res.status(401).json({ error: 'Invalid credentials' });
        }
        
        // VULNERABLE: Using hardcoded secret
        const accessToken = jwt.sign(
            { userId: user.id, username: user.username, role: user.role },
            JWT_SECRET,  // Hardcoded!
            { expiresIn: '1h' }
        );
        
        const refreshToken = jwt.sign(
            { userId: user.id, tokenType: 'refresh' },
            REFRESH_SECRET,  // Hardcoded!
            { expiresIn: '30d' }
        );
        
        // VULNERABLE: Debug logging with secrets
        console.log('Login successful:', {
            username: user.username,
            accessToken: accessToken,
            jwtSecret: JWT_SECRET,  // Exposed in logs!
            refreshSecret: REFRESH_SECRET
        });
        
        res.json({ accessToken, refreshToken, user: { id: user.id, username: user.username } });
        
    } catch (error) {
        // VULNERABLE: Error logging with secrets
        console.error('Login failed:', {
            error: error.message,
            jwtSecret: JWT_SECRET,  // Exposed in error logs!
            stack: error.stack
        });
        res.status(500).json({ error: 'Internal server error' });
    }
});

// VULNERABLE: Admin endpoint with different hardcoded secret
app.post('/admin/login', async (req, res) => {
    try {
        const { username, password } = req.body;
        
        const admin = await Admin.findOne({ username });
        if (!admin || !await bcrypt.compare(password, admin.password)) {
            return res.status(401).json({ error: 'Invalid admin credentials' });
        }
        
        // VULNERABLE: Different hardcoded secret for admin
        const adminToken = jwt.sign(
            { adminId: admin.id, username: admin.username, role: 'admin' },
            ADMIN_SECRET,  // Yet another hardcoded secret!
            { expiresIn: '2h' }
        );
        
        res.json({ token: adminToken });
        
    } catch (error) {
        console.error('Admin login failed with secret:', ADMIN_SECRET, error);
        res.status(500).json({ error: 'Internal server error' });
    }
});

// VULNERABLE: Token verification middleware
function authenticateToken(req, res, next) {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];
    
    if (!token) {
        return res.sendStatus(401);
    }
    
    try {
        // VULNERABLE: Using hardcoded secret for verification
        const decoded = jwt.verify(token, JWT_SECRET);
        req.user = decoded;
        
        // VULNERABLE: Logging token and secret
        console.log('Token verified successfully:', {
            token: token,
            secret: JWT_SECRET,  // Exposed!
            decoded: decoded
        });
        
        next();
    } catch (error) {
        console.error('Token verification failed:', {
            token: token,
            secret: JWT_SECRET,  // Exposed in error!
            error: error.message
        });
        res.sendStatus(403);
    }
}

// VULNERABLE: Debug endpoint that exposes secrets
app.get('/debug/config', (req, res) => {
    res.json({
        jwtSecret: JWT_SECRET,  // Completely exposed!
        refreshSecret: REFRESH_SECRET,
        adminSecret: ADMIN_SECRET,
        environment: process.env.NODE_ENV
    });
});

app.listen(3000, () => {
    console.log('Server running on port 3000');
    console.log('JWT Secret:', JWT_SECRET);  // Exposed in startup logs!
});
// SECURE: Express.js app with proper JWT secret management
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const winston = require('winston');
const rateLimit = require('express-rate-limit');
const helmet = require('helmet');
const app = express();

// SECURE: Environment variable validation
const requiredEnvVars = ['JWT_SECRET', 'JWT_REFRESH_SECRET', 'JWT_ISSUER', 'JWT_AUDIENCE'];
const missingVars = requiredEnvVars.filter(envVar => !process.env[envVar]);
if (missingVars.length > 0) {
    console.error('Missing required environment variables:', missingVars);
    process.exit(1);
}

// Validate secret strength
if (process.env.JWT_SECRET.length < 32 || process.env.JWT_REFRESH_SECRET.length < 32) {
    console.error('JWT secrets must be at least 32 characters long');
    process.exit(1);
}

// SECURE: Logger configuration without secret exposure
const logger = winston.createLogger({
    level: process.env.LOG_LEVEL || 'info',
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.errors({ stack: true }),
        winston.format.json(),
        winston.format.printf(info => {
            // Filter out any potential secrets from logs
            const filtered = { ...info };
            ['password', 'secret', 'token', 'key'].forEach(field => {
                if (filtered[field]) {
                    filtered[field] = '[REDACTED]';
                }
            });
            return JSON.stringify(filtered);
        })
    ),
    transports: [
        new winston.transports.File({ filename: 'app.log' }),
        new winston.transports.Console({
            format: winston.format.simple()
        })
    ]
});

// Security middleware
app.use(helmet());
app.use(express.json({ limit: '10mb' }));

// Rate limiting for authentication endpoints
const authLimiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 5, // 5 attempts per window
    message: { error: 'Too many authentication attempts, please try again later' },
    standardHeaders: true,
    legacyHeaders: false
});

// SECURE: Token service class
class TokenService {
    constructor() {
        this.jwtSecret = process.env.JWT_SECRET;
        this.refreshSecret = process.env.JWT_REFRESH_SECRET;
        this.issuer = process.env.JWT_ISSUER;
        this.audience = process.env.JWT_AUDIENCE;
        this.blacklistedTokens = new Set(); // In production, use Redis
    }

    generateTokenPair(user) {
        const tokenId = require('crypto').randomUUID();
        
        const accessToken = jwt.sign(
            {
                userId: user.id,
                username: user.username,
                role: user.role,
                tokenType: 'access',
                jti: tokenId
            },
            this.jwtSecret,
            {
                expiresIn: process.env.JWT_EXPIRES_IN || '15m',
                issuer: this.issuer,
                audience: this.audience
            }
        );
        
        const refreshToken = jwt.sign(
            {
                userId: user.id,
                tokenType: 'refresh',
                jti: tokenId
            },
            this.refreshSecret,
            {
                expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
                issuer: this.issuer
            }
        );
        
        return { accessToken, refreshToken, tokenId };
    }

    verifyAccessToken(token) {
        if (this.blacklistedTokens.has(token)) {
            throw new Error('Token has been revoked');
        }
        
        return jwt.verify(token, this.jwtSecret, {
            issuer: this.issuer,
            audience: this.audience
        });
    }

    verifyRefreshToken(token) {
        if (this.blacklistedTokens.has(token)) {
            throw new Error('Token has been revoked');
        }
        
        return jwt.verify(token, this.refreshSecret, {
            issuer: this.issuer
        });
    }

    revokeToken(token) {
        this.blacklistedTokens.add(token);
        // In production, store in Redis with TTL
    }
}

const tokenService = new TokenService();

// SECURE: Login endpoint
app.post('/login', authLimiter, async (req, res) => {
    try {
        const { username, password } = req.body;
        
        if (!username || !password) {
            logger.warn('Login attempt with missing credentials', {
                ip: req.ip,
                userAgent: req.get('User-Agent')
            });
            return res.status(400).json({ error: 'Username and password required' });
        }
        
        const user = await User.findOne({ username });
        if (!user || !await bcrypt.compare(password, user.password)) {
            logger.warn('Failed login attempt', {
                username: username,
                ip: req.ip,
                userAgent: req.get('User-Agent')
            });
            return res.status(401).json({ error: 'Invalid credentials' });
        }
        
        const { accessToken, refreshToken, tokenId } = tokenService.generateTokenPair(user);
        
        // SECURE: Success logging without exposing tokens or secrets
        logger.info('User login successful', {
            userId: user.id,
            username: user.username,
            tokenId: tokenId,
            ip: req.ip
        });
        
        // Set refresh token in httpOnly cookie for better security
        res.cookie('refreshToken', refreshToken, {
            httpOnly: true,
            secure: process.env.NODE_ENV === 'production',
            sameSite: 'strict',
            maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
        });
        
        res.json({
            accessToken,
            user: {
                id: user.id,
                username: user.username,
                role: user.role
            },
            expiresIn: process.env.JWT_EXPIRES_IN || '15m'
        });
        
    } catch (error) {
        // SECURE: Error logging without exposing secrets
        logger.error('Login endpoint error', {
            error: error.message,
            ip: req.ip,
            // Never log tokens or secrets
        });
        res.status(500).json({ error: 'Internal server error' });
    }
});

// SECURE: Token refresh endpoint
app.post('/refresh', async (req, res) => {
    try {
        const refreshToken = req.cookies.refreshToken;
        
        if (!refreshToken) {
            return res.status(401).json({ error: 'Refresh token required' });
        }
        
        const decoded = tokenService.verifyRefreshToken(refreshToken);
        
        // Revoke old refresh token
        tokenService.revokeToken(refreshToken);
        
        // Get user data and generate new tokens
        const user = await User.findById(decoded.userId);
        if (!user) {
            return res.status(401).json({ error: 'User not found' });
        }
        
        const { accessToken, refreshToken: newRefreshToken, tokenId } = tokenService.generateTokenPair(user);
        
        // Set new refresh token in cookie
        res.cookie('refreshToken', newRefreshToken, {
            httpOnly: true,
            secure: process.env.NODE_ENV === 'production',
            sameSite: 'strict',
            maxAge: 7 * 24 * 60 * 60 * 1000
        });
        
        logger.info('Token refresh successful', {
            userId: user.id,
            newTokenId: tokenId
        });
        
        res.json({ accessToken });
        
    } catch (error) {
        logger.error('Token refresh failed', {
            error: error.message,
            errorType: error.name
        });
        
        if (error.name === 'TokenExpiredError') {
            return res.status(401).json({ error: 'Refresh token expired' });
        }
        
        res.status(401).json({ error: 'Invalid refresh token' });
    }
});

// SECURE: Authentication middleware
function authenticateToken(req, res, next) {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];
    
    if (!token) {
        logger.warn('Authentication attempt without token', {
            ip: req.ip,
            path: req.path
        });
        return res.status(401).json({ error: 'Access token required' });
    }
    
    try {
        const decoded = tokenService.verifyAccessToken(token);
        
        if (decoded.tokenType !== 'access') {
            throw new Error('Invalid token type');
        }
        
        req.user = {
            userId: decoded.userId,
            username: decoded.username,
            role: decoded.role
        };
        
        // SECURE: Success logging without token or secret exposure
        logger.debug('Token authentication successful', {
            userId: decoded.userId,
            path: req.path
        });
        
        next();
        
    } catch (error) {
        // SECURE: Error logging without exposing tokens or secrets
        logger.warn('Token authentication failed', {
            error: error.message,
            errorType: error.name,
            ip: req.ip,
            path: req.path
        });
        
        if (error.name === 'TokenExpiredError') {
            return res.status(401).json({ error: 'Token expired', code: 'TOKEN_EXPIRED' });
        } else if (error.name === 'JsonWebTokenError') {
            return res.status(401).json({ error: 'Invalid token', code: 'INVALID_TOKEN' });
        }
        
        res.status(401).json({ error: 'Authentication failed' });
    }
}

// SECURE: Logout endpoint
app.post('/logout', authenticateToken, (req, res) => {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];
    const refreshToken = req.cookies.refreshToken;
    
    // Revoke both tokens
    if (token) tokenService.revokeToken(token);
    if (refreshToken) tokenService.revokeToken(refreshToken);
    
    // Clear refresh token cookie
    res.clearCookie('refreshToken');
    
    logger.info('User logout successful', {
        userId: req.user.userId
    });
    
    res.json({ message: 'Logged out successfully' });
});

// SECURE: Protected route example
app.get('/profile', authenticateToken, async (req, res) => {
    try {
        const user = await User.findById(req.user.userId).select('-password');
        res.json(user);
    } catch (error) {
        logger.error('Profile fetch failed', {
            userId: req.user.userId,
            error: error.message
        });
        res.status(500).json({ error: 'Internal server error' });
    }
});

// SECURE: Health check endpoint without exposing secrets
app.get('/health', (req, res) => {
    res.json({
        status: 'healthy',
        timestamp: new Date().toISOString(),
        version: process.env.APP_VERSION || 'unknown',
        environment: process.env.NODE_ENV || 'unknown'
        // Never include secrets or tokens in health checks
    });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    // SECURE: Startup logging without exposing secrets
    logger.info('Server started', {
        port: PORT,
        environment: process.env.NODE_ENV,
        version: process.env.APP_VERSION
        // Never log secrets during startup
    });
});

💡 Why This Fix Works

The vulnerable application hardcodes JWT secrets in source code and exposes them through debug logging and error messages. The secure version uses environment variables, proper validation, secure logging practices, and implements comprehensive token management without exposing sensitive information.

Why it happens

Developers often hardcode JWT signing secrets directly in authentication middleware, token generation functions, or configuration modules. This commonly occurs when using libraries like jsonwebtoken where the secret is passed as a string literal instead of being loaded from environment variables. The secret becomes visible in source code, version control, and compiled applications.

Root causes

Hardcoded JWT Secrets in Source Code

Developers often hardcode JWT signing secrets directly in authentication middleware, token generation functions, or configuration modules. This commonly occurs when using libraries like jsonwebtoken where the secret is passed as a string literal instead of being loaded from environment variables. The secret becomes visible in source code, version control, and compiled applications.

Preview example – JAVASCRIPT
// VULNERABLE: JWT secret hardcoded in authentication middleware
const jwt = require('jsonwebtoken');
const express = require('express');

// Hardcoded secret - NEVER do this!
const JWT_SECRET = 'super_secret_jwt_key_12345';
const REFRESH_SECRET = 'refresh_token_secret_67890';

function generateToken(user) {
    return jwt.sign(
        { userId: user.id, username: user.username, role: user.role },
        JWT_SECRET,  // Exposed secret!
        { expiresIn: '1h' }
    );
}

function verifyToken(token) {
    return jwt.verify(token, JWT_SECRET);  // Same exposed secret!
}

Configuration Files with Exposed Secrets

JWT secrets are frequently stored in configuration files like config.json, constants.js, or environment configuration modules that get committed to version control. These configuration files often contain multiple secrets and are shared across development teams, making JWT secrets accessible to anyone with repository access.

Preview example – JAVASCRIPT
// config/auth.js - VULNERABLE configuration file
module.exports = {
  jwt: {
    accessTokenSecret: 'access_token_secret_production_key_123',
    refreshTokenSecret: 'refresh_token_secret_production_key_456',
    issuer: 'company-app',
    audience: 'company-users',
    expiresIn: '15m',
    refreshExpiresIn: '7d'
  },
  
  oauth: {
    google: {
      clientId: 'google-client-id-12345',
      clientSecret: 'google-client-secret-67890'  // Also exposed!
    }
  }
};

// Usage in auth service
const config = require('./config/auth');
const token = jwt.sign(payload, config.jwt.accessTokenSecret);

Debug Logging and Error Exposure

JWT secrets can be inadvertently exposed through debug logging, error messages, or development tools. This happens when authentication errors include the secret in stack traces, when debug middleware logs token verification details, or when development servers expose configuration in error pages or API responses.

Preview example – JAVASCRIPT
// VULNERABLE: Debug logging that exposes JWT secrets
const jwt = require('jsonwebtoken');

function authenticateToken(req, res, next) {
    const authHeader = req.headers['authorization'];
    const token = authHeader && authHeader.split(' ')[1];
    
    if (!token) {
        return res.sendStatus(401);
    }
    
    try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        req.user = decoded;
        
        // VULNERABLE: Debug logging with secret exposure
        console.log('Token verification successful:', {
            token: token,
            secret: process.env.JWT_SECRET,  // Exposed in logs!
            decoded: decoded
        });
        
        next();
    } catch (error) {
        // VULNERABLE: Error with secret in details
        console.error('JWT verification failed:', {
            error: error.message,
            token: token,
            secret: process.env.JWT_SECRET,  // Exposed in error logs!
            stack: error.stack
        });
        
        res.sendStatus(403);
    }
}

Fixes

1

Use Environment Variables and Secure Key Management

Store JWT secrets in environment variables and never hardcode them in source files. Use strong, randomly generated secrets with sufficient entropy. Implement proper secret rotation mechanisms and consider using dedicated secret management services like AWS Secrets Manager, Azure Key Vault, or HashiCorp Vault for production environments.

View implementation – JAVASCRIPT
// SECURE: JWT implementation using environment variables
const jwt = require('jsonwebtoken');
const crypto = require('crypto');

// Validate JWT secret at startup
const JWT_SECRET = process.env.JWT_SECRET;
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;

if (!JWT_SECRET || !REFRESH_SECRET) {
    console.error('Missing required JWT secrets in environment variables');
    process.exit(1);
}

// Validate secret strength
if (JWT_SECRET.length < 32 || REFRESH_SECRET.length < 32) {
    console.error('JWT secrets must be at least 32 characters long');
    process.exit(1);
}

class TokenService {
    generateAccessToken(user) {
        return jwt.sign(
            { 
                userId: user.id, 
                username: user.username, 
                role: user.role,
                tokenType: 'access'
            },
            JWT_SECRET,
            { 
                expiresIn: process.env.JWT_EXPIRES_IN || '15m',
                issuer: process.env.JWT_ISSUER || 'app',
                audience: process.env.JWT_AUDIENCE || 'users'
            }
        );
    }

    generateRefreshToken(user) {
        return jwt.sign(
            { 
                userId: user.id,
                tokenType: 'refresh'
            },
            REFRESH_SECRET,
            { 
                expiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d',
                issuer: process.env.JWT_ISSUER || 'app'
            }
        );
    }

    verifyAccessToken(token) {
        return jwt.verify(token, JWT_SECRET);
    }

    verifyRefreshToken(token) {
        return jwt.verify(token, REFRESH_SECRET);
    }
}
2

Implement Secure Token Validation Middleware

Create authentication middleware that properly handles JWT verification without exposing secrets in logs or error messages. Implement proper error handling that provides useful debugging information without compromising security. Use structured logging that excludes sensitive data.

View implementation – JAVASCRIPT
// SECURE: Authentication middleware without secret exposure
const jwt = require('jsonwebtoken');
const winston = require('winston');

// Configure secure logger
const logger = winston.createLogger({
    level: 'info',
    format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.errors({ stack: true }),
        winston.format.json()
    ),
    transports: [
        new winston.transports.File({ filename: 'auth.log' })
    ]
});

class AuthMiddleware {
    constructor(tokenService) {
        this.tokenService = tokenService;
    }

    authenticate(req, res, next) {
        const authHeader = req.headers['authorization'];
        const token = authHeader && authHeader.split(' ')[1];

        if (!token) {
            logger.warn('Authentication attempt without token', {
                ip: req.ip,
                userAgent: req.get('User-Agent'),
                path: req.path
            });
            return res.status(401).json({ error: 'Access token required' });
        }

        try {
            const decoded = this.tokenService.verifyAccessToken(token);
            
            // Validate token type
            if (decoded.tokenType !== 'access') {
                throw new Error('Invalid token type');
            }
            
            req.user = {
                userId: decoded.userId,
                username: decoded.username,
                role: decoded.role
            };

            // SECURE: Log success without exposing secrets
            logger.info('Authentication successful', {
                userId: decoded.userId,
                username: decoded.username,
                ip: req.ip,
                path: req.path
            });

            next();
        } catch (error) {
            // SECURE: Error handling without exposing secrets
            logger.error('Authentication failed', {
                error: error.message,
                errorType: error.name,
                ip: req.ip,
                path: req.path,
                // Never log the token or secret
                tokenPresent: !!token
            });

            if (error.name === 'TokenExpiredError') {
                return res.status(401).json({ 
                    error: 'Token expired',
                    code: 'TOKEN_EXPIRED'
                });
            } else if (error.name === 'JsonWebTokenError') {
                return res.status(401).json({ 
                    error: 'Invalid token',
                    code: 'INVALID_TOKEN'
                });
            } else {
                return res.status(401).json({ 
                    error: 'Authentication failed',
                    code: 'AUTH_FAILED'
                });
            }
        }
    }

    requireRole(allowedRoles) {
        return (req, res, next) => {
            if (!req.user) {
                return res.status(401).json({ error: 'Authentication required' });
            }

            if (!allowedRoles.includes(req.user.role)) {
                logger.warn('Authorization failed - insufficient role', {
                    userId: req.user.userId,
                    currentRole: req.user.role,
                    requiredRoles: allowedRoles,
                    path: req.path
                });
                return res.status(403).json({ error: 'Insufficient permissions' });
            }

            next();
        };
    }
}
3

Implement Token Rotation and Secret Management

Implement proper token rotation strategies and secret management practices. Use short-lived access tokens with refresh tokens, implement secret rotation mechanisms, and consider using asymmetric key pairs (RS256) for better security. Set up monitoring for token usage patterns and implement anomaly detection.

View implementation – JAVASCRIPT
// SECURE: Advanced token management with rotation
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const NodeCache = require('node-cache');

// Token blacklist cache (for logout/rotation)
const tokenBlacklist = new NodeCache({ stdTTL: 60 * 60 * 24 }); // 24 hours

class SecureTokenService {
    constructor() {
        this.validateSecrets();
        this.currentSecretVersion = process.env.JWT_SECRET_VERSION || '1';
        this.secretRotationInterval = 24 * 60 * 60 * 1000; // 24 hours
        
        // Support for secret rotation with versioning
        this.secrets = new Map();
        this.loadSecrets();
        
        // Schedule secret rotation check
        setInterval(() => this.checkSecretRotation(), 60 * 60 * 1000); // Check hourly
    }

    validateSecrets() {
        const requiredSecrets = ['JWT_SECRET', 'JWT_REFRESH_SECRET'];
        const missingSecrets = requiredSecrets.filter(secret => !process.env[secret]);
        
        if (missingSecrets.length > 0) {
            throw new Error(`Missing JWT secrets: ${missingSecrets.join(', ')}`);
        }

        // Validate secret strength
        if (process.env.JWT_SECRET.length < 32) {
            throw new Error('JWT_SECRET must be at least 32 characters long');
        }
    }

    loadSecrets() {
        // Load current secret
        this.secrets.set(this.currentSecretVersion, {
            access: process.env.JWT_SECRET,
            refresh: process.env.JWT_REFRESH_SECRET,
            createdAt: new Date()
        });

        // Load previous secret for rotation support
        if (process.env.JWT_SECRET_PREVIOUS) {
            const previousVersion = (parseInt(this.currentSecretVersion) - 1).toString();
            this.secrets.set(previousVersion, {
                access: process.env.JWT_SECRET_PREVIOUS,
                refresh: process.env.JWT_REFRESH_SECRET_PREVIOUS,
                createdAt: new Date(Date.now() - this.secretRotationInterval)
            });
        }
    }

    generateTokenPair(user, sessionId = null) {
        const tokenId = crypto.randomUUID();
        const currentSecret = this.secrets.get(this.currentSecretVersion);
        
        const accessToken = jwt.sign(
            {
                userId: user.id,
                username: user.username,
                role: user.role,
                tokenType: 'access',
                tokenId: tokenId,
                secretVersion: this.currentSecretVersion,
                sessionId: sessionId
            },
            currentSecret.access,
            {
                expiresIn: '15m',
                issuer: process.env.JWT_ISSUER,
                audience: process.env.JWT_AUDIENCE,
                jwtid: tokenId
            }
        );

        const refreshToken = jwt.sign(
            {
                userId: user.id,
                tokenType: 'refresh',
                tokenId: tokenId,
                secretVersion: this.currentSecretVersion
            },
            currentSecret.refresh,
            {
                expiresIn: '7d',
                issuer: process.env.JWT_ISSUER,
                jwtid: tokenId
            }
        );

        return { accessToken, refreshToken, tokenId };
    }

    verifyToken(token, tokenType = 'access') {
        if (this.isTokenBlacklisted(token)) {
            throw new Error('Token has been revoked');
        }

        // Try to verify with current secret first
        let decoded;
        let secretVersion = this.currentSecretVersion;
        
        try {
            const currentSecret = this.secrets.get(this.currentSecretVersion);
            const secret = tokenType === 'access' ? currentSecret.access : currentSecret.refresh;
            decoded = jwt.verify(token, secret);
        } catch (error) {
            // If verification fails, try previous secret (for rotation period)
            if (error.name === 'JsonWebTokenError' && this.secrets.size > 1) {
                const previousVersion = (parseInt(this.currentSecretVersion) - 1).toString();
                const previousSecret = this.secrets.get(previousVersion);
                
                if (previousSecret) {
                    const secret = tokenType === 'access' ? previousSecret.access : previousSecret.refresh;
                    decoded = jwt.verify(token, secret);
                    secretVersion = previousVersion;
                }
            }
            
            if (!decoded) {
                throw error;
            }
        }

        // Validate token type
        if (decoded.tokenType !== tokenType) {
            throw new Error(`Invalid token type. Expected: ${tokenType}`);
        }

        // Check if token was issued with old secret and needs refresh
        if (secretVersion !== this.currentSecretVersion) {
            decoded._requiresRefresh = true;
        }

        return decoded;
    }

    refreshTokens(refreshToken) {
        const decoded = this.verifyToken(refreshToken, 'refresh');
        
        // Blacklist the old refresh token
        this.blacklistToken(refreshToken);
        
        // Generate new token pair
        const user = { id: decoded.userId }; // Fetch full user data from DB
        return this.generateTokenPair(user);
    }

    blacklistToken(token) {
        try {
            const decoded = jwt.decode(token);
            if (decoded && decoded.exp) {
                const ttl = decoded.exp - Math.floor(Date.now() / 1000);
                if (ttl > 0) {
                    tokenBlacklist.set(token, true, ttl);
                }
            }
        } catch (error) {
            // Token invalid, but blacklist anyway
            tokenBlacklist.set(token, true, 60 * 60 * 24); // 24 hours
        }
    }

    isTokenBlacklisted(token) {
        return tokenBlacklist.has(token);
    }

    checkSecretRotation() {
        const currentSecret = this.secrets.get(this.currentSecretVersion);
        const age = Date.now() - currentSecret.createdAt.getTime();
        
        if (age > this.secretRotationInterval) {
            console.warn('JWT secret rotation required. Current secret age:', age);
            // Implement secret rotation logic here
            // This would typically involve updating environment variables
            // and reloading secrets from a secret management service
        }
    }

    getTokenMetrics() {
        return {
            currentSecretVersion: this.currentSecretVersion,
            secretCount: this.secrets.size,
            blacklistedTokens: tokenBlacklist.keys().length,
            secretAge: Date.now() - this.secrets.get(this.currentSecretVersion).createdAt.getTime()
        };
    }
}

Detect This Vulnerability in Your Code

Sourcery automatically identifies jwt secret keys exposed in node.js applications and many other security issues in your codebase.