/** * Token Manager for JWT access tokens and refresh tokens * Implements secure session management with token rotation */ const jwt = require('jsonwebtoken'); const crypto = require('crypto'); const { hashValue, verifyHash, generateToken } = require('./encryption'); const ACCESS_TOKEN_TTL = 15 * 60; // 15 minutes in seconds const REFRESH_TOKEN_TTL = 7 * 24 * 60 * 60; // 7 days in seconds const REFRESH_TOKEN_BYTES = 64; // 128 character hex string let jwtSecret = null; /** * Initialize token manager with JWT secret * @param {string} secret - JWT signing secret */ function initTokenManager(secret) { if (!secret || typeof secret !== 'string') { throw new Error('JWT secret is required'); } jwtSecret = secret; console.log('✅ Token manager initialized'); } /** * Generate device fingerprint from request * @param {Object} req - HTTP request object * @returns {string} Device fingerprint (32 character hex) */ function generateDeviceFingerprint(req) { const components = [ req.headers['user-agent'] || '', req.headers['accept-language'] || '', req.ip || req.connection?.remoteAddress || '', req.headers['x-forwarded-for'] || '' ]; return crypto .createHash('sha256') .update(components.join('|')) .digest('hex') .substring(0, 32); } /** * Generate JWT access token * @param {Object} payload - Token payload (userId, email, role, plan) * @param {Object} options - Token options * @returns {string} JWT token */ function generateAccessToken(payload, options = {}) { if (!jwtSecret) { throw new Error('Token manager not initialized'); } const jti = crypto.randomUUID(); const now = Math.floor(Date.now() / 1000); const tokenPayload = { jti, userId: payload.userId, email: payload.email, role: payload.role || 'user', plan: payload.plan || 'hobby', iat: now, exp: now + (options.ttl || ACCESS_TOKEN_TTL) }; return jwt.sign(tokenPayload, jwtSecret, { algorithm: 'HS256' }); } /** * Verify and decode JWT access token * @param {string} token - JWT token to verify * @returns {Object|null} Decoded token payload or null if invalid */ function verifyAccessToken(token) { if (!jwtSecret) { throw new Error('Token manager not initialized'); } try { const decoded = jwt.verify(token, jwtSecret, { algorithms: ['HS256'] }); return decoded; } catch (error) { if (error.name === 'TokenExpiredError') { return { expired: true, error: 'Token expired' }; } if (error.name === 'JsonWebTokenError') { return { invalid: true, error: 'Invalid token' }; } return null; } } /** * Generate refresh token * @returns {Object} Object with token and tokenHash */ function generateRefreshToken() { const token = generateToken(REFRESH_TOKEN_BYTES); const { hash, salt } = hashValue(token); return { token, tokenHash: `${salt}:${hash}` }; } /** * Verify refresh token against stored hash * @param {string} token - Refresh token to verify * @param {string} storedHash - Stored hash in format "salt:hash" * @returns {boolean} True if token matches hash */ function verifyRefreshToken(token, storedHash) { if (!token || !storedHash) { return false; } try { const [salt, hash] = storedHash.split(':'); if (!salt || !hash) { return false; } return verifyHash(token, hash, salt); } catch (error) { return false; } } /** * Extract token from Authorization header or cookie * @param {Object} req - HTTP request object * @param {string} cookieName - Name of the cookie containing token * @returns {string|null} Token or null */ function extractToken(req, cookieName = 'access_token') { // Check Authorization header first (Bearer token) const authHeader = req.headers.authorization; if (authHeader && authHeader.startsWith('Bearer ')) { return authHeader.substring(7); } // Check cookie if (req.headers.cookie) { const cookies = parseCookies(req.headers.cookie); return cookies[cookieName] || null; } return null; } /** * Parse cookie header * @param {string} cookieHeader - Cookie header string * @returns {Object} Parsed cookies */ function parseCookies(cookieHeader) { const cookies = {}; if (!cookieHeader) { return cookies; } cookieHeader.split(';').forEach(cookie => { const [name, ...rest] = cookie.split('='); if (name && rest.length > 0) { cookies[name.trim()] = rest.join('=').trim(); } }); return cookies; } /** * Create secure cookie string * @param {string} name - Cookie name * @param {string} value - Cookie value * @param {Object} options - Cookie options * @returns {string} Set-Cookie header value */ function createSecureCookie(name, value, options = {}) { const parts = [`${name}=${value}`]; if (options.maxAge) { parts.push(`Max-Age=${options.maxAge}`); } if (options.path) { parts.push(`Path=${options.path}`); } else { parts.push('Path=/'); } if (options.httpOnly !== false) { parts.push('HttpOnly'); } if (options.secure) { parts.push('Secure'); } if (options.sameSite) { parts.push(`SameSite=${options.sameSite}`); } else { parts.push('SameSite=Strict'); } return parts.join('; '); } /** * Get token TTL values * @returns {Object} Object with accessTokenTTL and refreshTokenTTL */ function getTokenTTL() { return { accessTokenTTL: ACCESS_TOKEN_TTL, refreshTokenTTL: REFRESH_TOKEN_TTL }; } /** * Check if token manager is initialized * @returns {boolean} */ function isTokenManagerInitialized() { return jwtSecret !== null; } module.exports = { initTokenManager, generateDeviceFingerprint, generateAccessToken, verifyAccessToken, generateRefreshToken, verifyRefreshToken, extractToken, createSecureCookie, parseCookies, getTokenTTL, isTokenManagerInitialized };