/** * Middleware de protection CSRF (Cross-Site Request Forgery) * Génère et valide des tokens CSRF pour les requêtes sensibles */ const crypto = require('crypto'); const { logger } = require('../config/logs'); // Stockage en mémoire des tokens CSRF (en production, utiliser Redis ou une base de données) const csrfTokens = new Map(); // Durée de vie d'un token CSRF (30 minutes) const TOKEN_LIFETIME = 30 * 60 * 1000; /** * Génère un token CSRF pour une session */ const generateCsrfToken = (sessionId) => { const token = crypto.randomBytes(32).toString('hex'); const expiresAt = Date.now() + TOKEN_LIFETIME; csrfTokens.set(sessionId, { token, expiresAt }); // Nettoyer les tokens expirés toutes les 5 minutes setInterval(() => { const now = Date.now(); for (const [id, data] of csrfTokens.entries()) { if (data.expiresAt < now) { csrfTokens.delete(id); } } }, 5 * 60 * 1000); return token; }; /** * Valide un token CSRF */ const validateCsrfToken = (sessionId, token) => { const storedData = csrfTokens.get(sessionId); if (!storedData) { return false; } if (storedData.expiresAt < Date.now()) { csrfTokens.delete(sessionId); return false; } return storedData.token === token; }; /** * Middleware pour ajouter le token CSRF à la requête */ const csrfTokenMiddleware = (req, res, next) => { // Générer un ID de session si nécessaire if (!req.session) { return next(); } const sessionId = req.session.id || req.sessionID; // Générer ou récupérer le token CSRF let csrfToken = null; const storedData = csrfTokens.get(sessionId); if (storedData && storedData.expiresAt > Date.now()) { csrfToken = storedData.token; } else { csrfToken = generateCsrfToken(sessionId); } // Ajouter le token aux locals pour l'utiliser dans les vues res.locals.csrfToken = csrfToken; // Ajouter une méthode helper req.csrfToken = () => csrfToken; next(); }; /** * Middleware de validation CSRF pour les méthodes POST, PUT, DELETE */ const csrfProtectionMiddleware = (req, res, next) => { // Ignorer les requêtes GET, HEAD, OPTIONS if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) { return next(); } // Ignorer certaines routes (API publiques, webhooks, etc.) const exemptPaths = [ '/auth/activedirectory/callback', '/auth/discord/callback', '/api/webhook' ]; if (exemptPaths.some(path => req.path.startsWith(path))) { return next(); } // Vérifier si la session existe if (!req.session) { logger.warn(`CSRF protection: No session found for ${req.method} ${req.path} from ${req.ip}`); return res.status(403).json({ error: 'Session invalide', code: 'NO_SESSION' }); } const sessionId = req.session.id || req.sessionID; // Récupérer le token CSRF de la requête const token = req.body._csrf || req.query._csrf || req.headers['x-csrf-token'] || req.headers['csrf-token']; if (!token) { logger.warn(`CSRF protection: No token provided for ${req.method} ${req.path} from ${req.ip}`); return res.status(403).json({ error: 'Token CSRF manquant', code: 'CSRF_TOKEN_MISSING' }); } // Valider le token if (!validateCsrfToken(sessionId, token)) { logger.warn(`CSRF protection: Invalid token for ${req.method} ${req.path} from ${req.ip}`); return res.status(403).json({ error: 'Token CSRF invalide ou expiré', code: 'CSRF_TOKEN_INVALID' }); } next(); }; /** * Route pour obtenir un nouveau token CSRF */ const getCsrfToken = (req, res) => { const sessionId = req.session?.id || req.sessionID; if (!sessionId) { return res.status(400).json({ error: 'Session invalide' }); } const token = generateCsrfToken(sessionId); res.json({ csrfToken: token }); }; module.exports = { csrfTokenMiddleware, csrfProtectionMiddleware, getCsrfToken, generateCsrfToken, validateCsrfToken };