const csrf = require('csurf'); const logger = require('../utils/logger'); // Create CSRF protection middleware const csrfProtection = csrf({ cookie: { httpOnly: true, secure: process.env.NODE_ENV === 'production' && process.env.HTTPS === 'true', sameSite: 'strict', maxAge: 3600000 // 1 hour } }); /** * Middleware to handle CSRF token errors */ const csrfErrorHandler = (err, req, res, next) => { if (err.code === 'EBADCSRFTOKEN') { logger.warn('CSRF token validation failed', { ip: req.ip, path: req.path, method: req.method, userAgent: req.get('user-agent') }); return res.status(403).json({ success: false, error: 'Invalid CSRF token', message: 'Your session has expired or the request is invalid. Please refresh the page and try again.' }); } next(err); }; /** * Middleware to inject CSRF token into response * Adds csrfToken to all JSON responses and as a header */ const injectCsrfToken = (req, res, next) => { // Add token to response locals for template rendering res.locals.csrfToken = req.csrfToken(); // Override json method to automatically include CSRF token const originalJson = res.json.bind(res); res.json = function(data) { if (data && typeof data === 'object' && !data.csrfToken) { data.csrfToken = res.locals.csrfToken; } return originalJson(data); }; next(); }; /** * Skip CSRF protection for specific routes (e.g., webhooks, public APIs) */ const csrfExemptRoutes = [ '/api/health', '/api/metrics', '/api/config', '/api/auth/login', // Login uses credentials for authentication '/api/auth/logout', // Logout is an authentication action '/api/auth/session', // Session check is read-only '/api/representatives/postal/', // Read-only operation '/api/campaigns/public' // Public read operations ]; const conditionalCsrfProtection = (req, res, next) => { // Skip CSRF for exempt routes const isExempt = csrfExemptRoutes.some(route => req.path.startsWith(route)); // Skip CSRF for GET, HEAD, OPTIONS (safe methods) const isSafeMethod = ['GET', 'HEAD', 'OPTIONS'].includes(req.method); if (isExempt || isSafeMethod) { return next(); } // Log CSRF validation attempt for debugging console.log('=== CSRF VALIDATION ==='); console.log('Method:', req.method); console.log('Path:', req.path); console.log('Body Token:', req.body?._csrf ? 'YES' : 'NO'); console.log('Header Token:', req.headers['x-csrf-token'] ? 'YES' : 'NO'); console.log('CSRF Cookie:', req.cookies['_csrf'] ? 'YES' : 'NO'); console.log('Session ID:', req.session?.id || 'NO_SESSION'); console.log('======================='); // Apply CSRF protection for state-changing operations csrfProtection(req, res, (err) => { if (err) { console.log('=== CSRF ERROR ==='); console.log('Error Message:', err.message); console.log('Error Code:', err.code); console.log('Path:', req.path); console.log('=================='); logger.warn('CSRF token validation failed'); csrfErrorHandler(err, req, res, next); } else { logger.info('CSRF validation passed for:', req.path); next(); } }); }; /** * Helper to get CSRF token for client-side use */ const getCsrfToken = (req, res) => { try { // Generate a CSRF token if one doesn't exist const token = req.csrfToken(); console.log('=== CSRF TOKEN GENERATION ==='); console.log('Token Length:', token?.length || 0); console.log('Has Token:', !!token); console.log('Session ID:', req.session?.id || 'NO_SESSION'); console.log('Cookie will be set:', !!req.cookies); console.log('============================='); res.json({ csrfToken: token }); } catch (error) { console.log('=== CSRF TOKEN ERROR ==='); console.log('Error:', error.message); console.log('Stack:', error.stack); console.log('========================'); logger.error('Failed to generate CSRF token', { error: error.message, stack: error.stack }); res.status(500).json({ error: 'Failed to generate CSRF token' }); } }; module.exports = { csrfProtection, csrfErrorHandler, injectCsrfToken, conditionalCsrfProtection, getCsrfToken };