/** * Enhanced Security Middleware * Implements account lockout, password expiry, and session management */ const { db } = require('../database/db'); const { ACCOUNT_LOCKOUT, PASSWORD_EXPIRY, SESSION_POLICY } = require('../utils/passwordPolicy'); const SecurityAuditLogger = require('../utils/securityAudit'); const logger = require('../utils/logger'); /** * Check if account is locked */ async function checkAccountLockout(userId) { return new Promise((resolve, reject) => { db.get( 'SELECT locked_until FROM users WHERE id = ?', [userId], (err, user) => { if (err) return reject(err); if (!user) return resolve({ locked: false }); if (user.locked_until) { const lockoutEnd = new Date(user.locked_until); const now = new Date(); if (now < lockoutEnd) { const remainingMinutes = Math.ceil((lockoutEnd - now) / 60000); return resolve({ locked: true, remainingMinutes, message: `Account locked. Try again in ${remainingMinutes} minutes.` }); } else { // Lockout expired, clear it db.run('UPDATE users SET locked_until = NULL, failed_login_attempts = 0 WHERE id = ?', [userId]); return resolve({ locked: false }); } } resolve({ locked: false }); } ); }); } /** * Record failed login attempt */ async function recordFailedLogin(username, ip, userAgent) { try { // Log to audit await SecurityAuditLogger.logLoginFailure(username, 'Invalid credentials', { ip, userAgent }); // Get user ID const user = await new Promise((resolve, reject) => { db.get('SELECT id, failed_login_attempts FROM users WHERE username = ? OR email = ?', [username, username], (err, row) => { if (err) reject(err); else resolve(row); }); }); if (!user) return; const failedAttempts = (user.failed_login_attempts || 0) + 1; // Update failed attempts await new Promise((resolve, reject) => { db.run( 'UPDATE users SET failed_login_attempts = ?, last_failed_login = ? WHERE id = ?', [failedAttempts, new Date().toISOString(), user.id], (err) => err ? reject(err) : resolve() ); }); // Check if lockout threshold reached if (failedAttempts >= ACCOUNT_LOCKOUT.maxFailedAttempts) { const lockUntil = new Date(Date.now() + ACCOUNT_LOCKOUT.lockoutDuration).toISOString(); await new Promise((resolve, reject) => { db.run( 'UPDATE users SET locked_until = ? WHERE id = ?', [lockUntil, user.id], (err) => err ? reject(err) : resolve() ); }); await SecurityAuditLogger.logAccountLockout(user.id, { ip, userAgent, failedAttempts }); logger.warn(`Account locked for user ${username} after ${failedAttempts} failed attempts`); } } catch (error) { logger.error('Error recording failed login:', error); } } /** * Clear failed login attempts on successful login */ async function clearFailedAttempts(userId) { return new Promise((resolve, reject) => { db.run( 'UPDATE users SET failed_login_attempts = 0, last_failed_login = NULL WHERE id = ?', [userId], (err) => err ? reject(err) : resolve() ); }); } /** * Check if password has expired */ async function checkPasswordExpiry(userId) { if (!PASSWORD_EXPIRY.enabled) { return { expired: false, warning: false }; } return new Promise((resolve, reject) => { db.get( 'SELECT password_changed_at, password_expires_at FROM users WHERE id = ?', [userId], (err, user) => { if (err) return reject(err); if (!user) return resolve({ expired: false, warning: false }); const now = new Date(); let expiryDate; if (user.password_expires_at) { expiryDate = new Date(user.password_expires_at); } else if (user.password_changed_at) { expiryDate = new Date(user.password_changed_at); expiryDate.setDate(expiryDate.getDate() + PASSWORD_EXPIRY.expiryDays); } else { // No password change date, set expiry from now expiryDate = new Date(); expiryDate.setDate(expiryDate.getDate() + PASSWORD_EXPIRY.expiryDays); } const daysUntilExpiry = Math.ceil((expiryDate - now) / (24 * 60 * 60 * 1000)); if (daysUntilExpiry <= 0) { return resolve({ expired: true, warning: false, message: 'Your password has expired. Please change it to continue.', gracePeriodDays: PASSWORD_EXPIRY.gracePeriodDays }); } if (daysUntilExpiry <= PASSWORD_EXPIRY.warningDays) { return resolve({ expired: false, warning: true, daysRemaining: daysUntilExpiry, message: `Your password will expire in ${daysUntilExpiry} days. Please change it soon.` }); } resolve({ expired: false, warning: false }); } ); }); } /** * Update password expiry date */ async function updatePasswordExpiry(userId) { if (!PASSWORD_EXPIRY.enabled) return; const now = new Date(); const expiryDate = new Date(now); expiryDate.setDate(expiryDate.getDate() + PASSWORD_EXPIRY.expiryDays); return new Promise((resolve, reject) => { db.run( 'UPDATE users SET password_changed_at = ?, password_expires_at = ? WHERE id = ?', [now.toISOString(), expiryDate.toISOString(), userId], (err) => err ? reject(err) : resolve() ); }); } /** * Middleware: Check account lockout before authentication */ const enforceAccountLockout = async (req, res, next) => { const { username } = req.body; if (!username) { return next(); } try { // Get user const user = await new Promise((resolve, reject) => { db.get('SELECT id FROM users WHERE username = ? OR email = ?', [username, username], (err, row) => { if (err) reject(err); else resolve(row); }); }); if (!user) { return next(); // User doesn't exist, let auth handle it } // Check lockout const lockoutStatus = await checkAccountLockout(user.id); if (lockoutStatus.locked) { return res.status(423).json({ error: lockoutStatus.message, remainingMinutes: lockoutStatus.remainingMinutes, locked: true }); } next(); } catch (error) { logger.error('Account lockout check error:', error); next(); } }; /** * Middleware: Check password expiry after authentication */ const enforcePasswordExpiry = async (req, res, next) => { if (!req.user || !req.user.userId) { return next(); } try { const expiryStatus = await checkPasswordExpiry(req.user.userId); if (expiryStatus.expired) { return res.status(403).json({ error: expiryStatus.message, passwordExpired: true, gracePeriodDays: expiryStatus.gracePeriodDays, requirePasswordChange: true }); } if (expiryStatus.warning) { // Add warning header but allow request res.setHeader('X-Password-Expiry-Warning', expiryStatus.message); res.setHeader('X-Password-Days-Remaining', expiryStatus.daysRemaining.toString()); } next(); } catch (error) { logger.error('Password expiry check error:', error); next(); } }; /** * Manage active sessions */ async function createSession(userId, token, req) { const ip = req.ip || req.headers['x-forwarded-for'] || req.connection.remoteAddress; const userAgent = req.headers['user-agent']; const expiresAt = new Date(Date.now() + SESSION_POLICY.absoluteTimeout); // Check concurrent sessions const activeSessions = await new Promise((resolve, reject) => { db.all( 'SELECT COUNT(*) as count FROM active_sessions WHERE user_id = ? AND expires_at > ?', [userId, new Date().toISOString()], (err, rows) => err ? reject(err) : resolve(rows[0].count) ); }); if (activeSessions >= SESSION_POLICY.maxConcurrentSessions) { // Remove oldest session await new Promise((resolve, reject) => { db.run( 'DELETE FROM active_sessions WHERE id IN (SELECT id FROM active_sessions WHERE user_id = ? ORDER BY last_activity ASC LIMIT 1)', [userId], (err) => err ? reject(err) : resolve() ); }); } // Create new session return new Promise((resolve, reject) => { db.run( 'INSERT INTO active_sessions (user_id, session_token, ip_address, user_agent, expires_at) VALUES (?, ?, ?, ?, ?)', [userId, token, ip, userAgent, expiresAt.toISOString()], (err) => err ? reject(err) : resolve() ); }); } /** * Update session activity */ async function updateSessionActivity(token) { return new Promise((resolve, reject) => { db.run( 'UPDATE active_sessions SET last_activity = ? WHERE session_token = ?', [new Date().toISOString(), token], (err) => err ? reject(err) : resolve() ); }); } /** * Cleanup expired sessions */ async function cleanupExpiredSessions() { return new Promise((resolve, reject) => { db.run( 'DELETE FROM active_sessions WHERE expires_at < ?', [new Date().toISOString()], function(err) { if (err) reject(err); else { if (this.changes > 0) { logger.info(`Cleaned up ${this.changes} expired sessions`); } resolve(this.changes); } } ); }); } // Run session cleanup every hour setInterval(cleanupExpiredSessions, 60 * 60 * 1000); module.exports = { checkAccountLockout, recordFailedLogin, clearFailedAttempts, checkPasswordExpiry, updatePasswordExpiry, enforceAccountLockout, enforcePasswordExpiry, createSession, updateSessionActivity, cleanupExpiredSessions };