const jwt = require('jsonwebtoken'); // JWT secrets - REQUIRED in production, fail-fast if not set const JWT_ACCESS_SECRET = process.env.JWT_ACCESS_SECRET; const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET; // Fail-fast: Require both secrets to be set at startup if (!JWT_ACCESS_SECRET) { throw new Error('FATAL: JWT_ACCESS_SECRET environment variable is required. Set it in your .env file.'); } if (!JWT_REFRESH_SECRET) { throw new Error('FATAL: JWT_REFRESH_SECRET environment variable is required. Set it in your .env file.'); } // Token expiry configuration const JWT_EXPIRY = process.env.JWT_EXPIRY || '24h'; const REFRESH_TOKEN_EXPIRY = process.env.REFRESH_TOKEN_EXPIRY || '7d'; /** * SECURITY NOTES: * - Access tokens should be short-lived (default: 24h) * - Refresh tokens should be stored server-side (database) or as HttpOnly cookies * - Refresh tokens should be rotated on each use to prevent replay attacks * - Never store refresh tokens in localStorage or any client-side storage * - Consider implementing refresh token families for better security */ /** * Middleware to verify JWT token */ function verifyToken(req, res, next) { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN if (!token) { return res.status(401).json({ success: false, message: 'Access token required' }); } try { const decoded = jwt.verify(token, JWT_ACCESS_SECRET); req.user = decoded; next(); } catch (error) { if (error.name === 'TokenExpiredError') { return res.status(401).json({ success: false, message: 'Token expired', expired: true }); } return res.status(403).json({ success: false, message: 'Invalid token' }); } } /** * Generate access token */ function generateAccessToken(user) { return jwt.sign( { username: user.username, id: user.id }, JWT_ACCESS_SECRET, { expiresIn: JWT_EXPIRY } ); } /** * Generate refresh token * NOTE: Refresh tokens MUST be stored server-side (in database) or as HttpOnly cookies. * They should be rotated on each use to prevent replay attacks. */ function generateRefreshToken(user) { return jwt.sign( { username: user.username, id: user.id, type: 'refresh' }, JWT_REFRESH_SECRET, { expiresIn: REFRESH_TOKEN_EXPIRY } ); } /** * Verify refresh token and return user data * NOTE: In production, refresh tokens should be validated against a server-side store * and rotated on each use. */ function verifyRefreshToken(token) { try { const decoded = jwt.verify(token, JWT_REFRESH_SECRET); if (decoded.type !== 'refresh') { throw new Error('Invalid token type'); } return decoded; } catch (error) { throw error; } } /** * Middleware to require an admin role. Must run after verifyToken. */ async function requireAdmin(req, res, next) { try { const userService = require('../services/userService'); const current = req.user?.username ? await userService.findByUsername(req.user.username) : null; if (!current || Number(current.id) !== Number(req.user?.id) || current.username !== req.user?.username || current.role !== 'admin') { return res.status(403).json({ success: false, message: 'Admin access required' }); } req.user = { ...req.user, id: current.id, username: current.username, role: current.role }; next(); } catch (_error) { return res.status(503).json({ success: false, message: 'Admin authorization unavailable' }); } } async function findConfiguredOperator(user) { const configuredUsername = String(process.env.AUTH_USERNAME || '').trim(); if (!configuredUsername || user?.username !== configuredUsername) return null; const current = await require('../services/userService').findByUsername(user.username); if (!current || current.username !== user.username || Number(current.id) !== Number(user.id) || current.role !== 'admin') return null; return current; } // Must follow verifyToken. Never trust roles or identity from a stale JWT. async function requireConfiguredOperator(req, res, next) { const message = res.locals.operatorDeniedMessage || 'Ingen adgang til projektdata'; const denied = () => res.status(403).json({ success: false, error: message, ...(res.locals.operatorDeniedCode ? { code: res.locals.operatorDeniedCode } : {}) }); try { const current = await findConfiguredOperator(req.user); if (!current) return denied(); req.user = { ...req.user, id: current.id, username: current.username, role: current.role }; return next(); } catch (_error) { return res.status(503).json({ success: false, error: 'Operator authorization unavailable' }); } } module.exports = { verifyToken, requireAdmin, findConfiguredOperator, requireConfiguredOperator, generateAccessToken, generateRefreshToken, verifyRefreshToken };