const csrf = require("csrf"); const cookieParser = require("cookie-parser"); const logger = require("../utils/logger"); // Initialize CSRF token generator const tokens = new csrf(); // Use persistent secret from environment variable to prevent token invalidation on restart const secret = process.env.CSRF_SECRET; if (!secret) { const errorMsg = "CSRF_SECRET environment variable is required."; logger.error(errorMsg); throw new Error(errorMsg); } if (secret.length < 32) { const errorMsg = "CSRF_SECRET must be at least 32 characters for security"; logger.error(errorMsg); throw new Error(errorMsg); } // CSRF middleware using double submit cookie pattern const csrfProtection = (req, res, next) => { // Skip CSRF for safe methods if (["GET", "HEAD", "OPTIONS"].includes(req.method)) { return next(); } // Get token from header or body const token = req.headers["x-csrf-token"] || req.body.csrfToken || req.query.csrfToken; // Get token from cookie const cookieToken = req.cookies && req.cookies["csrf-token"]; // Verify both tokens exist and match if (!token || !cookieToken || token !== cookieToken) { return res.status(403).json({ error: "Invalid CSRF token", code: "CSRF_TOKEN_MISMATCH", }); } // Verify token is valid if (!tokens.verify(secret, token)) { return res.status(403).json({ error: "Invalid CSRF token", code: "CSRF_TOKEN_INVALID", }); } next(); }; // Middleware to generate and send CSRF token const generateCSRFToken = (req, res, next) => { const token = tokens.create(secret); // Set token in cookie (httpOnly for security) res.cookie("csrf-token", token, { httpOnly: true, secure: process.env.NODE_ENV !== "dev", sameSite: "strict", maxAge: 60 * 60 * 1000, // 1 hour }); // Also provide token in header for client to use res.set("X-CSRF-Token", token); // Make token available to response res.locals.csrfToken = token; next(); }; // Route to get CSRF token (for initial page loads) const getCSRFToken = (req, res) => { const token = tokens.create(secret); res.cookie("csrf-token", token, { httpOnly: true, secure: process.env.NODE_ENV !== "dev", sameSite: "strict", maxAge: 60 * 60 * 1000, }); res.set("X-CSRF-Token", token); res.status(204).send(); }; module.exports = { csrfProtection, generateCSRFToken, getCSRFToken, cookieParser: cookieParser(), };