Files
SkyArtShop/backend/middleware/bruteForceProtection.js

153 lines
3.3 KiB
JavaScript
Raw Permalink Normal View History

2026-01-04 17:52:37 -06:00
/**
* 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,
};