const crypto = require('crypto'); const { logAudit } = require('../services/auditService'); // ───────────────────────────────────────────────────────────────────────────── // CSRF Middleware // Protects state-changing routes (POST, PUT, DELETE) from cross-site request // forgery by validating tokens stored in session cookie. // ───────────────────────────────────────────────────────────────────────────── const CSRF_HEADER_NAME = 'x-csrf-token'; // CSRF cookie httpOnly setting - configurable via environment variable // Default: false — the SPA uses a double-submit pattern (reads token from // document.cookie and sends it in the x-csrf-token header), which requires // JavaScript access to the cookie. Setting httpOnly=true would break this flow. // For server-rendered apps, set CSRF_HTTP_ONLY=true for additional protection. const CSRF_HTTP_ONLY = process.env.CSRF_HTTP_ONLY === 'true'; // defaults to false for SPA // CSRF cookie sameSite setting - configurable via environment variable // Options: 'lax', 'strict', 'none' // Default: 'strict' (most secure) // Set CSRF_SAME_SITE=lax for SPA cross-site scenarios const CSRF_SAME_SITE = process.env.CSRF_SAME_SITE || 'strict'; // CSRF cookie secure setting - configurable via environment variable // Default: true (only send over HTTPS) // Set CSRF_SECURE=false for HTTP development (NOT recommended for production) const CSRF_SECURE = process.env.CSRF_SECURE !== 'false'; // defaults to true // CSRF cookie name - configurable via environment variable // Default: 'bt_csrf_token' // Use CSRF_COOKIE_NAME to customize for multi-app deployments const CSRF_COOKIE_NAME = process.env.CSRF_COOKIE_NAME || 'bt_csrf_token'; // Generate a cryptographically secure CSRF token function generateCsrfToken() { return crypto.randomBytes(32).toString('hex'); } /** * Get or create CSRF token for the current session. * Tokens are stored in HTTP-only cookies for automatic validation. */ function getCsrfToken(req, res) { let token = req.cookies?.[CSRF_COOKIE_NAME]; if (!token) { token = generateCsrfToken(); res.cookie(CSRF_COOKIE_NAME, token, { httpOnly: CSRF_HTTP_ONLY, sameSite: CSRF_SAME_SITE, secure: CSRF_SECURE && req.secure, path: '/', }); } return token; } /** * Validate CSRF token from request. * Tokens can be provided via: * - x-csrf-token header (API clients) * - csrf_token query parameter (form submissions) * - csrf_token body field (form submissions) */ function validateCsrfToken(req) { const cookieToken = req.cookies?.[CSRF_COOKIE_NAME]; if (!cookieToken) return false; const headerToken = req.headers?.[CSRF_HEADER_NAME]; if (headerToken && headerToken === cookieToken) return true; const queryToken = req.query?.csrf_token; if (queryToken && queryToken === cookieToken) return true; const bodyToken = req.body?.csrf_token; if (bodyToken && bodyToken === cookieToken) return true; return false; } /** * CSRF middleware - validates tokens for state-changing methods. * Skips validation for: GET, HEAD, OPTIONS (safe methods) * Requires token for: POST, PUT, DELETE, PATCH (state-changing) */ function csrfMiddleware(req, res, next) { // Exempt login endpoint - no session exists yet to hijack // Check both originalUrl and path for mounted routers if (req.originalUrl === '/api/auth/login' || req.path === '/login' || req.path === '/api/auth/login') { return next(); } // Only validate state-changing methods if (!['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method)) { return next(); } // Skip validation for OPTIONS (preflight) if (req.method === 'OPTIONS') { return next(); } // Allow API routes to opt-in explicitly via a flag // This allows flexibility for routes that use alternate auth (e.g., API keys) if (req.csrfSkip) { return next(); } // Validate the CSRF token if (!validateCsrfToken(req)) { logAudit({ user_id: req.user?.id || null, action: 'csrf.failure', ip_address: req.ip, user_agent: req.get('user-agent') }); return res.status(403).json({ error: 'CSRF token validation failed', message: 'Your session has expired or this request may be fraudulent. Please refresh the page and try again.', code: 'CSRF_INVALID', }); } next(); } /** * Attach CSRF token to response locals for template rendering. * Frontend can access req.csrfToken() in templates. */ function csrfTokenProvider(req, res, next) { res.locals.csrfToken = getCsrfToken(req, res); next(); } module.exports = { CSRF_COOKIE_NAME, CSRF_HEADER_NAME, CSRF_HTTP_ONLY, CSRF_SAME_SITE, CSRF_SECURE, generateCsrfToken, getCsrfToken, validateCsrfToken, csrfMiddleware, csrfTokenProvider, };