/** * Authentication Security Utilities * Provides enhanced security features for authentication */ const { db } = require('../database/db'); const { formatBoolean } = require('./dbCompat'); const logger = require('./logger'); // Configuration constants const MAX_LOGIN_ATTEMPTS = 5; const LOCKOUT_DURATION = 30 * 60 * 1000; // 30 minutes in milliseconds const ATTEMPT_WINDOW = 15 * 60 * 1000; // 15 minutes window for counting attempts /** * Track failed login attempt * @param {string} identifier - Username or email * @param {string} ipAddress - IP address of the attempt * @param {string} userAgent - User agent string */ async function trackFailedAttempt(identifier, ipAddress, userAgent) { try { await db('login_attempts').insert({ identifier, ip_address: ipAddress, user_agent: userAgent, attempt_time: new Date().toISOString(), success: false }); // Log security event logger.warn('Failed login attempt', { identifier, ipAddress, userAgent, timestamp: new Date().toISOString() }); } catch (error) { logger.error('Error tracking failed login attempt:', error); } } /** * Track successful login * @param {string} identifier - Username or email * @param {string} ipAddress - IP address * @param {string} userAgent - User agent string */ async function trackSuccessfulLogin(identifier, ipAddress, userAgent) { try { await db('login_attempts').insert({ identifier, ip_address: ipAddress, user_agent: userAgent, attempt_time: new Date().toISOString(), success: true }); // Clear old failed attempts for this user const cutoffTime = new Date(Date.now() - ATTEMPT_WINDOW); await db('login_attempts') .where('identifier', identifier) .where('success', formatBoolean(false)) .where('attempt_time', '<', cutoffTime.toISOString()) .delete(); } catch (error) { logger.error('Error tracking successful login:', error); } } /** * Check if account is locked due to too many failed attempts * @param {string} identifier - Username or email * @returns {Promise<{isLocked: boolean, remainingTime?: number}>} */ async function checkAccountLockout(identifier) { try { const recentWindow = new Date(Date.now() - ATTEMPT_WINDOW); // Get recent failed attempts const failedAttempts = await db('login_attempts') .where('identifier', identifier) .where('success', formatBoolean(false)) .where('attempt_time', '>=', recentWindow.toISOString()) .orderBy('attempt_time', 'desc') .limit(MAX_LOGIN_ATTEMPTS); if (failedAttempts.length >= MAX_LOGIN_ATTEMPTS) { // Check if still within lockout period const oldestAttempt = failedAttempts[failedAttempts.length - 1]; const lockoutEnd = new Date(oldestAttempt.attempt_time).getTime() + LOCKOUT_DURATION; const now = Date.now(); if (now < lockoutEnd) { return { isLocked: true, remainingTime: Math.ceil((lockoutEnd - now) / 1000) // seconds }; } } return { isLocked: false }; } catch (error) { logger.error('Error checking account lockout:', error); return { isLocked: false }; // Fail open to avoid locking users out due to errors } } /** * Check for suspicious login patterns * @param {string} identifier - Username or email * @param {string} ipAddress - Current IP address * @returns {Promise} - True if suspicious */ async function checkSuspiciousActivity(identifier, ipAddress) { try { // Check for rapid attempts from different IPs const recentWindow = new Date(Date.now() - 5 * 60 * 1000); // 5 minutes const recentAttempts = await db('login_attempts') .where('identifier', identifier) .where('attempt_time', '>=', recentWindow.toISOString()) .select('ip_address') .distinct('ip_address'); // If more than 3 different IPs in 5 minutes, it's suspicious if (recentAttempts.length > 3) { logger.warn('Suspicious login activity detected', { identifier, uniqueIPs: recentAttempts.length, currentIP: ipAddress }); return true; } return false; } catch (error) { logger.error('Error checking suspicious activity:', error); return false; } } /** * Get generic error message to prevent user enumeration * @returns {string} */ function getGenericAuthError() { return 'Invalid credentials'; } /** * Clean up old login attempts (should be run periodically) */ async function cleanupOldAttempts() { try { const cutoffDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); // 7 days const deleted = await db('login_attempts') .where('attempt_time', '<', cutoffDate.toISOString()) .delete(); if (deleted > 0) { logger.info(`Cleaned up ${deleted} old login attempts`); } } catch (error) { logger.error('Error cleaning up login attempts:', error); } } /** * Initialize cleanup job */ function initializeCleanupJob() { // Run cleanup every 24 hours setInterval(cleanupOldAttempts, 24 * 60 * 60 * 1000); // Run initial cleanup cleanupOldAttempts(); } module.exports = { trackFailedAttempt, trackSuccessfulLogin, checkAccountLockout, checkSuspiciousActivity, getGenericAuthError, initializeCleanupJob, MAX_LOGIN_ATTEMPTS, LOCKOUT_DURATION };