143 lines
4.2 KiB
JavaScript
143 lines
4.2 KiB
JavaScript
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
|
|
};
|