auth/src/middleware/rateLimitMiddleware.js

327 lines
9.1 KiB
JavaScript

// src/middleware/rateLimitMiddleware.js
// === ADDED FOR RATE LIMITING ===
// Rate limiting middleware for OTP requests and verification
const { getRedisClient, isRedisReady } = require('../services/redisClient');
// In-memory fallback store (used when Redis is not available)
// Unified store for all rate limiting keys
const memoryStore = {};
// Clean up expired entries from memory store periodically
setInterval(() => {
const now = Date.now();
Object.keys(memoryStore).forEach((key) => {
if (memoryStore[key].expiresAt && memoryStore[key].expiresAt < now) {
delete memoryStore[key];
}
});
}, 60000); // Clean up every minute
// Configuration from environment variables
const config = {
// OTP request limits
OTP_REQ_PHONE_10MIN_LIMIT: parseInt(process.env.OTP_REQ_PHONE_10MIN_LIMIT || '3', 10),
OTP_REQ_PHONE_DAY_LIMIT: parseInt(process.env.OTP_REQ_PHONE_DAY_LIMIT || '10', 10),
OTP_REQ_IP_10MIN_LIMIT: parseInt(process.env.OTP_REQ_IP_10MIN_LIMIT || '20', 10),
OTP_REQ_IP_DAY_LIMIT: parseInt(process.env.OTP_REQ_IP_DAY_LIMIT || '100', 10),
// OTP verification limits
OTP_VERIFY_MAX_ATTEMPTS: parseInt(process.env.OTP_VERIFY_MAX_ATTEMPTS || '5', 10),
OTP_VERIFY_FAILED_PER_HOUR_LIMIT: parseInt(process.env.OTP_VERIFY_FAILED_PER_HOUR_LIMIT || '10', 10),
// OTP validity
OTP_TTL_SECONDS: parseInt(process.env.OTP_TTL_SECONDS || '120', 10), // 2 minutes
};
/**
* Helper: Increment counter in Redis or memory store
*/
async function incrementCounter(key, ttlSeconds) {
const redis = await getRedisClient();
if (isRedisReady() && redis) {
try {
const count = await redis.incr(key);
if (count === 1) {
// First increment, set TTL
await redis.expire(key, ttlSeconds);
}
return count;
} catch (err) {
console.error('Redis increment error, falling back to memory:', err);
// Fall through to memory store
}
}
// Memory store fallback
const now = Date.now();
if (!memoryStore[key]) {
memoryStore[key] = {
count: 0,
expiresAt: now + ttlSeconds * 1000,
};
}
memoryStore[key].count++;
return memoryStore[key].count;
}
/**
* Helper: Get counter value from Redis or memory store
*/
async function getCounter(key) {
const redis = await getRedisClient();
if (isRedisReady() && redis) {
try {
const count = await redis.get(key);
return count ? parseInt(count, 10) : 0;
} catch (err) {
console.error('Redis get error, falling back to memory:', err);
// Fall through to memory store
}
}
// Memory store fallback
if (memoryStore[key] && memoryStore[key].expiresAt > Date.now()) {
return memoryStore[key].count || 0;
}
return 0;
}
/**
* Helper: Check if key exists in Redis or memory store
*/
async function exists(key) {
const redis = await getRedisClient();
if (isRedisReady() && redis) {
try {
const result = await redis.exists(key);
return result === 1;
} catch (err) {
console.error('Redis exists error, falling back to memory:', err);
// Fall through to memory store
}
}
// Memory store fallback
if (memoryStore[key] && memoryStore[key].expiresAt > Date.now()) {
return true;
}
return false;
}
/**
* Helper: Set key with TTL in Redis or memory store
*/
async function setWithTTL(key, value, ttlSeconds) {
const redis = await getRedisClient();
if (isRedisReady() && redis) {
try {
await redis.setEx(key, ttlSeconds, value);
return true;
} catch (err) {
console.error('Redis setEx error, falling back to memory:', err);
// Fall through to memory store
}
}
// Memory store fallback
memoryStore[key] = {
value,
expiresAt: Date.now() + ttlSeconds * 1000,
};
return true;
}
/**
* === ADDED FOR 2-MIN OTP VALIDITY & NO-RESEND ===
* Middleware: Check if there's an active OTP for the phone number
* Prevents sending a new OTP if one is already active (within 2 minutes)
*/
async function checkActiveOtpForPhone(req, res, next) {
try {
const { phone_number } = req.body;
if (!phone_number) {
return next(); // Let validation handle this
}
// Normalize phone (same logic as in routes)
const normalizedPhone = phone_number.trim().replace(/\s+/g, '');
const phone = normalizedPhone.startsWith('+')
? normalizedPhone
: (normalizedPhone.length === 10 ? '+91' + normalizedPhone : normalizedPhone);
const activeOtpKey = `otp_active:phone:${phone}`;
const hasActiveOtp = await exists(activeOtpKey);
if (hasActiveOtp) {
return res.status(429).json({
success: false,
message: 'An OTP is already active. Please wait a moment before requesting a new one.',
});
}
next();
} catch (err) {
console.error('checkActiveOtpForPhone error:', err);
// On error, allow the request to proceed (fail open)
next();
}
}
/**
* === ADDED FOR RATE LIMITING ===
* Middleware: Rate limit OTP requests per phone number
*/
async function rateLimitRequestOtpByPhone(req, res, next) {
try {
const { phone_number } = req.body;
if (!phone_number) {
return next(); // Let validation handle this
}
// Normalize phone (same logic as in routes)
const normalizedPhone = phone_number.trim().replace(/\s+/g, '');
const phone = normalizedPhone.startsWith('+')
? normalizedPhone
: (normalizedPhone.length === 10 ? '+91' + normalizedPhone : normalizedPhone);
// Check 10-minute limit
const key10min = `otp_req:phone:${phone}:10min`;
const count10min = await incrementCounter(key10min, 600); // 10 minutes = 600 seconds
if (count10min > config.OTP_REQ_PHONE_10MIN_LIMIT) {
return res.status(429).json({
success: false,
message: 'Too many OTP requests. Please try again later.',
});
}
// Check 24-hour limit
const keyDay = `otp_req:phone:${phone}:day`;
const countDay = await incrementCounter(keyDay, 86400); // 24 hours = 86400 seconds
if (countDay > config.OTP_REQ_PHONE_DAY_LIMIT) {
return res.status(429).json({
success: false,
message: 'Too many OTP requests. Please try again later.',
});
}
next();
} catch (err) {
console.error('rateLimitRequestOtpByPhone error:', err);
// On error, allow the request to proceed (fail open)
next();
}
}
/**
* === ADDED FOR RATE LIMITING ===
* Middleware: Rate limit OTP requests per IP address
*/
async function rateLimitRequestOtpByIp(req, res, next) {
try {
// Get client IP (considering proxies)
const ip = req.ip || req.connection.remoteAddress || 'unknown';
// Check 10-minute limit
const key10min = `otp_req:ip:${ip}:10min`;
const count10min = await incrementCounter(key10min, 600); // 10 minutes = 600 seconds
if (count10min > config.OTP_REQ_IP_10MIN_LIMIT) {
return res.status(429).json({
success: false,
message: 'Too many OTP requests. Please try again later.',
});
}
// Check 24-hour limit
const keyDay = `otp_req:ip:${ip}:day`;
const countDay = await incrementCounter(keyDay, 86400); // 24 hours = 86400 seconds
if (countDay > config.OTP_REQ_IP_DAY_LIMIT) {
return res.status(429).json({
success: false,
message: 'Too many OTP requests. Please try again later.',
});
}
next();
} catch (err) {
console.error('rateLimitRequestOtpByIp error:', err);
// On error, allow the request to proceed (fail open)
next();
}
}
/**
* === ADDED FOR OTP ATTEMPT LIMIT ===
* Middleware: Rate limit failed OTP verification attempts per phone
*/
async function rateLimitVerifyOtpByPhone(req, res, next) {
try {
const { phone_number } = req.body;
if (!phone_number) {
return next(); // Let validation handle this
}
// Normalize phone (same logic as in routes)
const normalizedPhone = phone_number.trim().replace(/\s+/g, '');
const phone = normalizedPhone.startsWith('+')
? normalizedPhone
: (normalizedPhone.length === 10 ? '+91' + normalizedPhone : normalizedPhone);
// Check failed verification limit per hour
const key = `otp_verify_failed:phone:${phone}:hour`;
const count = await getCounter(key);
if (count >= config.OTP_VERIFY_FAILED_PER_HOUR_LIMIT) {
return res.status(429).json({
success: false,
message: 'Too many attempts. Please try again later.',
});
}
next();
} catch (err) {
console.error('rateLimitVerifyOtpByPhone error:', err);
// On error, allow the request to proceed (fail open)
next();
}
}
/**
* Helper: Mark active OTP (called after OTP is created)
*/
async function markActiveOtp(phone, ttlSeconds) {
const key = `otp_active:phone:${phone}`;
await setWithTTL(key, '1', ttlSeconds);
}
/**
* Helper: Increment failed verification counter
*/
async function incrementFailedVerify(phone) {
const key = `otp_verify_failed:phone:${phone}:hour`;
await incrementCounter(key, 3600); // 1 hour = 3600 seconds
}
module.exports = {
checkActiveOtpForPhone,
rateLimitRequestOtpByPhone,
rateLimitRequestOtpByIp,
rateLimitVerifyOtpByPhone,
markActiveOtp,
incrementFailedVerify,
config,
};