// 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' });
}
};
};