/** * Brute force protection middleware * Tracks failed login attempts and temporarily blocks IPs with too many failures */ const logger = require("../config/logger"); // Store failed attempts in memory (use Redis in production) const failedAttempts = new Map(); const blockedIPs = new Map(); // Configuration const MAX_FAILED_ATTEMPTS = 5; const BLOCK_DURATION = 15 * 60 * 1000; // 15 minutes const ATTEMPT_WINDOW = 15 * 60 * 1000; // 15 minutes const CLEANUP_INTERVAL = 60 * 1000; // 1 minute /** * Clean up old entries periodically */ const cleanup = () => { const now = Date.now(); // Clean up failed attempts for (const [ip, data] of failedAttempts.entries()) { if (now - data.firstAttempt > ATTEMPT_WINDOW) { failedAttempts.delete(ip); } } // Clean up blocked IPs for (const [ip, blockTime] of blockedIPs.entries()) { if (now - blockTime > BLOCK_DURATION) { blockedIPs.delete(ip); logger.info("IP unblocked after cooldown", { ip }); } } }; // Start cleanup interval setInterval(cleanup, CLEANUP_INTERVAL); /** * Record a failed login attempt * @param {string} ip - IP address */ const recordFailedAttempt = (ip) => { const now = Date.now(); if (!failedAttempts.has(ip)) { failedAttempts.set(ip, { count: 1, firstAttempt: now, }); } else { const data = failedAttempts.get(ip); // Reset if outside window if (now - data.firstAttempt > ATTEMPT_WINDOW) { data.count = 1; data.firstAttempt = now; } else { data.count++; } // Block if too many attempts if (data.count >= MAX_FAILED_ATTEMPTS) { blockedIPs.set(ip, now); logger.warn("IP blocked due to failed login attempts", { ip, attempts: data.count, }); } } }; /** * Reset failed attempts for an IP (on successful login) * @param {string} ip - IP address */ const resetFailedAttempts = (ip) => { failedAttempts.delete(ip); }; /** * Check if an IP is currently blocked * @param {string} ip - IP address * @returns {boolean} */ const isBlocked = (ip) => { if (!blockedIPs.has(ip)) { return false; } const blockTime = blockedIPs.get(ip); const now = Date.now(); // Check if block has expired if (now - blockTime > BLOCK_DURATION) { blockedIPs.delete(ip); return false; } return true; }; /** * Get remaining block time in seconds * @param {string} ip - IP address * @returns {number} Seconds remaining */ const getRemainingBlockTime = (ip) => { if (!blockedIPs.has(ip)) { return 0; } const blockTime = blockedIPs.get(ip); const elapsed = Date.now() - blockTime; const remaining = Math.max(0, BLOCK_DURATION - elapsed); return Math.ceil(remaining / 1000); }; /** * Middleware to check if IP is blocked */ const checkBlocked = (req, res, next) => { const ip = req.ip || req.connection.remoteAddress; if (isBlocked(ip)) { const remainingSeconds = getRemainingBlockTime(ip); logger.warn("Blocked IP attempted access", { ip, path: req.path }); return res.status(429).json({ success: false, message: `Too many failed attempts. Please try again in ${Math.ceil( remainingSeconds / 60 )} minutes.`, retryAfter: remainingSeconds, }); } next(); }; module.exports = { recordFailedAttempt, resetFailedAttempts, isBlocked, checkBlocked, getRemainingBlockTime, };