const express = require('express'); const router = express.Router(); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const { body, validationResult } = require('express-validator'); const { authLimiter } = require('../middleware/rateLimiter'); const { authenticate } = require('../middleware/auth'); const { db } = require('../database/db'); const logger = require('../utils/logger'); const { validatePassword, calculatePasswordStrength } = require('../utils/passwordPolicy'); const SecurityAuditLogger = require('../utils/securityAudit'); const { enforceAccountLockout, recordFailedLogin, clearFailedAttempts, updatePasswordExpiry, createSession, checkPasswordExpiry } = require('../middleware/securityEnhancements'); const JWT_SECRET = process.env.JWT_SECRET || 'change_this_in_production'; const JWT_EXPIRES_IN = '7d'; // Register - Controlled by DISABLE_SIGNUPS environment variable router.post('/register', [ body('username').trim().isLength({ min: 3, max: 50 }).isAlphanumeric(), body('email').isEmail().normalizeEmail(), body('password').notEmpty() ], async (req, res) => { const ip = req.ip || req.headers['x-forwarded-for'] || req.connection.remoteAddress; const userAgent = req.headers['user-agent']; // Check if signups are disabled (default: true) const disableSignups = process.env.DISABLE_SIGNUPS !== 'false'; if (disableSignups) { await SecurityAuditLogger.logAuthEvent(null, 'registration_attempt', 'blocked', { ip, userAgent, reason: 'Registration disabled' }); return res.status(403).json({ error: 'Registration is disabled. Contact an administrator to create your account.' }); } // If signups are enabled, proceed with registration (fallback for future flexibility) const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { username, email, password } = req.body; try { // Validate password against policy const passwordValidation = validatePassword(password, username, email); if (!passwordValidation.valid) { await SecurityAuditLogger.logAuthEvent(null, 'registration_attempt', 'failed', { ip, userAgent, username, reason: passwordValidation.errors.join(', ') }); return res.status(400).json({ error: 'Password does not meet requirements', details: passwordValidation.errors, strength: calculatePasswordStrength(password) }); } const hashedPassword = await bcrypt.hash(password, 10); const now = new Date().toISOString(); db.run( 'INSERT INTO users (username, email, password, role, must_change_password, password_changed_at, password_expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)', [username, email, hashedPassword, 'user', 0, now, null], async function(err) { if (err) { if (err.message.includes('UNIQUE')) { await SecurityAuditLogger.logAuthEvent(null, 'registration_attempt', 'failed', { ip, userAgent, username, reason: 'Duplicate username or email' }); return res.status(400).json({ error: 'Username or email already exists' }); } logger.error('Registration error:', err); return res.status(500).json({ error: 'Registration failed' }); } const userId = this.lastID; // Update password expiry await updatePasswordExpiry(userId); // Log successful registration await SecurityAuditLogger.logAuthEvent(userId, 'registration', 'success', { ip, userAgent, username }); const token = jwt.sign( { userId, role: 'user' }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN } ); // CWE-778: Log token issuance await SecurityAuditLogger.logTokenIssuance(userId, 'JWT', { ip, userAgent, expiresIn: JWT_EXPIRES_IN, purpose: 'registration' }); // Create session await createSession(userId, token, req); // Set secure HTTP-only cookie res.cookie('auth_token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days }); res.status(201).json({ message: 'Registration successful', token, user: { id: userId, username, email, role: 'user' } }); } ); } catch (error) { logger.error('Registration error:', error); await SecurityAuditLogger.logAuthEvent(null, 'registration_attempt', 'error', { ip, userAgent, error: error.message }); res.status(500).json({ error: 'Registration failed' }); } } ); // Login with strict rate limiting and account lockout router.post('/login', authLimiter, enforceAccountLockout, [ body('username').trim().notEmpty(), body('password').notEmpty() ], async (req, res) => { const ip = req.ip || req.headers['x-forwarded-for'] || req.connection.remoteAddress; const userAgent = req.headers['user-agent']; const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { username, password } = req.body; try { db.get( 'SELECT * FROM users WHERE username = ? OR email = ?', [username, username], async (err, user) => { if (err) { logger.error('Login error:', err); return res.status(500).json({ error: 'Login failed' }); } if (!user) { await recordFailedLogin(username, ip, userAgent); return res.status(401).json({ error: 'Invalid credentials' }); } // Check if user is active if (!user.is_active) { await SecurityAuditLogger.logLoginFailure(username, 'Account disabled', { ip, userAgent }); return res.status(403).json({ error: 'Account is disabled. Contact an administrator.' }); } const isValidPassword = await bcrypt.compare(password, user.password); if (!isValidPassword) { await recordFailedLogin(username, ip, userAgent); return res.status(401).json({ error: 'Invalid credentials' }); } // Clear failed attempts on successful password check await clearFailedAttempts(user.id); // Check password expiry const expiryStatus = await checkPasswordExpiry(user.id); if (expiryStatus.expired) { await SecurityAuditLogger.logLoginFailure(username, 'Password expired', { ip, userAgent }); return res.status(403).json({ error: expiryStatus.message, passwordExpired: true, requirePasswordChange: true }); } // Check if 2FA is enabled if (user.two_factor_enabled) { // Create temporary token for 2FA verification const tempToken = jwt.sign( { userId: user.id, temp: true, purpose: '2fa' }, JWT_SECRET, { expiresIn: '10m' } ); // CWE-778: Log temp token issuance for 2FA await SecurityAuditLogger.logTokenIssuance(user.id, 'TEMP_2FA', { ip, userAgent, expiresIn: '10m', purpose: '2fa' }); await SecurityAuditLogger.logAuthEvent(user.id, '2fa_required', 'pending', { ip, userAgent }); return res.json({ require2FA: true, tempToken, userId: user.id, passwordWarning: expiryStatus.warning ? expiryStatus.message : null }); } const token = jwt.sign( { userId: user.id, role: user.role }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN } ); // CWE-778: Log token issuance await SecurityAuditLogger.logTokenIssuance(user.id, 'JWT', { ip, userAgent, expiresIn: JWT_EXPIRES_IN, purpose: 'login' }); // Update last login db.run( 'UPDATE users SET last_login_at = ?, last_login_ip = ? WHERE id = ?', [new Date().toISOString(), ip, user.id] ); // Create session await createSession(user.id, token, req); // Log successful login await SecurityAuditLogger.logLoginSuccess(user.id, { ip, userAgent }); // Set secure HTTP-only cookie res.cookie('auth_token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days }); res.json({ message: 'Login successful', token, user: { id: user.id, username: user.username, email: user.email, role: user.role, must_change_password: user.must_change_password === 1 }, passwordWarning: expiryStatus.warning ? expiryStatus.message : null }); } ); } catch (error) { logger.error('Login error:', error); await SecurityAuditLogger.logAuthEvent(null, 'login_attempt', 'error', { ip, userAgent, error: error.message }); res.status(500).json({ error: 'Login failed' }); } } ); // Verify 2FA and complete login router.post('/verify-2fa', authLimiter, [ body('tempToken').notEmpty(), body('code').notEmpty() ], async (req, res) => { const ip = req.ip || req.headers['x-forwarded-for'] || req.connection.remoteAddress; const userAgent = req.headers['user-agent']; const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { tempToken, code } = req.body; try { // Verify temp token const decoded = jwt.verify(tempToken, JWT_SECRET); if (!decoded.temp || decoded.purpose !== '2fa') { return res.status(401).json({ error: 'Invalid token' }); } const speakeasy = require('speakeasy'); db.get( 'SELECT * FROM users WHERE id = ?', [decoded.userId], async (err, user) => { if (err || !user) { logger.error('2FA verify - user not found:', err); return res.status(401).json({ error: 'Invalid token' }); } if (!user.two_factor_enabled) { return res.status(400).json({ error: '2FA not enabled for this user' }); } // Check if it's a backup code db.get( 'SELECT id FROM two_factor_backup_codes WHERE user_id = ? AND code = ? AND used = 0', [user.id, code.toUpperCase()], async (err, backupCode) => { if (backupCode) { // Mark backup code as used db.run( 'UPDATE two_factor_backup_codes SET used = 1, used_at = CURRENT_TIMESTAMP WHERE id = ?', [backupCode.id] ); logger.info(`Backup code used for user ${user.id}`); // Log 2FA success with backup code await SecurityAuditLogger.log2FAEvent(user.id, 'backup_code_used', 'success', { ip, userAgent }); // Generate full token const token = jwt.sign( { userId: user.id, role: user.role }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN } ); // CWE-778: Log token issuance after 2FA backup code await SecurityAuditLogger.logTokenIssuance(user.id, 'JWT', { ip, userAgent, expiresIn: JWT_EXPIRES_IN, purpose: '2fa_backup_verification' }); // Update last login db.run( 'UPDATE users SET last_login_at = ?, last_login_ip = ? WHERE id = ?', [new Date().toISOString(), ip, user.id] ); // Create session await createSession(user.id, token, req); // Log successful login await SecurityAuditLogger.logLoginSuccess(user.id, { ip, userAgent, method: '2fa_backup' }); // Set secure HTTP-only cookie res.cookie('auth_token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days }); return res.json({ message: 'Login successful with backup code', token, user: { id: user.id, username: user.username, email: user.email, role: user.role, must_change_password: user.must_change_password === 1 } }); } // Verify TOTP code const verified = speakeasy.totp.verify({ secret: user.two_factor_secret, encoding: 'base32', token: code, window: 2 }); if (!verified) { await SecurityAuditLogger.log2FAEvent(user.id, 'totp_verification', 'failed', { ip, userAgent, reason: 'Invalid code' }); return res.status(400).json({ error: 'Invalid 2FA code' }); } // Log 2FA success await SecurityAuditLogger.log2FAEvent(user.id, 'totp_verification', 'success', { ip, userAgent }); // Generate full token const token = jwt.sign( { userId: user.id, role: user.role }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN } ); // CWE-778: Log token issuance after TOTP 2FA await SecurityAuditLogger.logTokenIssuance(user.id, 'JWT', { ip, userAgent, expiresIn: JWT_EXPIRES_IN, purpose: '2fa_totp_verification' }); // Update last login db.run( 'UPDATE users SET last_login_at = ?, last_login_ip = ? WHERE id = ?', [new Date().toISOString(), ip, user.id] ); // Create session await createSession(user.id, token, req); // Log successful login await SecurityAuditLogger.logLoginSuccess(user.id, { ip, userAgent, method: '2fa_totp' }); // Set secure HTTP-only cookie res.cookie('auth_token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days }); res.json({ message: 'Login successful', token, user: { id: user.id, username: user.username, email: user.email, role: user.role, must_change_password: user.must_change_password === 1 } }); } ); } ); } catch (error) { logger.error('2FA verify error:', error); if (error.name === 'TokenExpiredError') { return res.status(401).json({ error: '2FA session expired. Please login again' }); } res.status(500).json({ error: 'Failed to verify 2FA' }); } } ); // Change password with enhanced security router.post('/change-password', [ body('currentPassword').notEmpty(), body('newPassword').notEmpty() ], async (req, res) => { const token = req.headers.authorization?.split(' ')[1]; const ip = req.ip || req.headers['x-forwarded-for'] || req.connection.remoteAddress; const userAgent = req.headers['user-agent']; if (!token) { return res.status(401).json({ error: 'No token provided' }); } try { const decoded = jwt.verify(token, JWT_SECRET); const { currentPassword, newPassword } = req.body; db.get( 'SELECT * FROM users WHERE id = ?', [decoded.userId], async (err, user) => { if (err || !user) { return res.status(404).json({ error: 'User not found' }); } const isValidPassword = await bcrypt.compare(currentPassword, user.password); if (!isValidPassword) { await SecurityAuditLogger.logPasswordChange(user.id, 'failed', { ip, userAgent, reason: 'Incorrect current password' }); return res.status(401).json({ error: 'Current password is incorrect' }); } // Validate new password const passwordValidation = validatePassword(newPassword, user.username, user.email); if (!passwordValidation.valid) { await SecurityAuditLogger.logPasswordChange(user.id, 'failed', { ip, userAgent, reason: passwordValidation.errors.join(', ') }); return res.status(400).json({ error: 'New password does not meet requirements', details: passwordValidation.errors, strength: calculatePasswordStrength(newPassword) }); } // Check password history (prevent reuse of last 5 passwords) db.all( 'SELECT password_hash FROM password_history WHERE user_id = ? ORDER BY changed_at DESC LIMIT 5', [user.id], async (err, history) => { if (err) { logger.error('Password history check error:', err); return res.status(500).json({ error: 'Failed to change password' }); } // Check if new password matches any recent password for (const record of history || []) { const matches = await bcrypt.compare(newPassword, record.password_hash); if (matches) { await SecurityAuditLogger.logPasswordChange(user.id, 'failed', { ip, userAgent, reason: 'Password reused from history' }); return res.status(400).json({ error: 'Cannot reuse any of your last 5 passwords' }); } } const hashedPassword = await bcrypt.hash(newPassword, 10); // Save old password to history db.run( 'INSERT INTO password_history (user_id, password_hash) VALUES (?, ?)', [user.id, user.password] ); // Update password db.run( 'UPDATE users SET password = ?, must_change_password = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [hashedPassword, user.id], async (err) => { if (err) { logger.error('Password change error:', err); return res.status(500).json({ error: 'Failed to change password' }); } // Update password expiry await updatePasswordExpiry(user.id); // CWE-778: Revoke all tokens on password change (security best practice) await SecurityAuditLogger.logTokenRevocation(user.id, 'password_change', { ip, userAgent }); // Log successful password change await SecurityAuditLogger.logPasswordChange(user.id, 'success', { ip, userAgent }); res.json({ message: 'Password changed successfully' }); } ); } ); } ); } catch (error) { logger.error('Password change error:', error); res.status(401).json({ error: 'Invalid token' }); } } ); // Verify token router.get('/verify', async (req, res) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'No token provided' }); } try { const decoded = jwt.verify(token, JWT_SECRET); db.get( 'SELECT id, username, email, role, must_change_password, is_active FROM users WHERE id = ?', [decoded.userId], async (err, user) => { if (err || !user) { return res.status(401).json({ error: 'Invalid token' }); } if (!user.is_active) { return res.status(403).json({ error: 'Account is disabled' }); } // Check password expiry for warning const expiryStatus = await checkPasswordExpiry(user.id); res.json({ valid: true, user: { ...user, must_change_password: user.must_change_password === 1 }, passwordWarning: expiryStatus.warning ? expiryStatus.message : null, daysUntilExpiry: expiryStatus.daysRemaining || null }); } ); } catch (error) { res.status(401).json({ error: 'Invalid token' }); } }); // Check password strength router.post('/check-password-strength', [body('password').notEmpty()], (req, res) => { const { password, username, email } = req.body; const validation = validatePassword(password, username, email); const strength = calculatePasswordStrength(password); res.json({ valid: validation.valid, errors: validation.errors, strength: { score: strength.score, level: strength.level, feedback: strength.feedback } }); } ); // Get security status for current user router.get('/security-status', async (req, res) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'No token provided' }); } try { const decoded = jwt.verify(token, JWT_SECRET); db.get( 'SELECT id, username, two_factor_enabled, password_changed_at, password_expires_at, last_login_at, last_login_ip, failed_login_attempts FROM users WHERE id = ?', [decoded.userId], async (err, user) => { if (err || !user) { return res.status(401).json({ error: 'Invalid token' }); } // Get active sessions count const sessions = await new Promise((resolve, reject) => { db.all( 'SELECT COUNT(*) as count, MAX(last_activity) as last_activity FROM active_sessions WHERE user_id = ? AND expires_at > ?', [user.id, new Date().toISOString()], (err, rows) => err ? reject(err) : resolve(rows[0]) ); }); // Get recent security events let recentEvents = []; try { recentEvents = await SecurityAuditLogger.getUserSecurityEvents(user.id, 10); } catch (eventErr) { logger.error('Error fetching security events:', eventErr); } // Check password expiry const expiryStatus = await checkPasswordExpiry(user.id); res.json({ twoFactorEnabled: user.two_factor_enabled === 1, passwordAge: user.password_changed_at ? Math.floor((Date.now() - new Date(user.password_changed_at)) / (24 * 60 * 60 * 1000)) : null, passwordExpiry: expiryStatus, lastLogin: { timestamp: user.last_login_at, ip: user.last_login_ip }, activeSessions: sessions.count || 0, lastActivity: sessions.last_activity, failedLoginAttempts: user.failed_login_attempts || 0, recentEvents: Array.isArray(recentEvents) ? recentEvents.map(e => ({ type: e.event_type, status: e.status, timestamp: e.created_at })) : [] }); } ); } catch (error) { logger.error('Security status error:', error); res.status(401).json({ error: 'Invalid token' }); } }); // Logout endpoint - invalidate session and clear cookie router.post('/logout', authenticate, async (req, res) => { const token = req.headers.authorization?.replace('Bearer ', ''); const ip = req.ip || req.headers['x-forwarded-for'] || req.connection.remoteAddress; const userAgent = req.headers['user-agent']; try { // Delete the session from database await new Promise((resolve, reject) => { db.run( 'DELETE FROM active_sessions WHERE session_token = ?', [token], (err) => err ? reject(err) : resolve() ); }); // CWE-778: Log token revocation on logout await SecurityAuditLogger.logTokenRevocation(req.user.userId, 'user_logout', { ip, userAgent }); // Log logout event await SecurityAuditLogger.logAuthEvent(req.user.userId, 'logout', 'success', { ip, userAgent }); // Clear the HTTP-only cookie res.clearCookie('auth_token', { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict' }); res.json({ message: 'Logout successful' }); } catch (error) { logger.error('Logout error:', error); res.status(500).json({ error: 'Logout failed' }); } }); module.exports = router;