const express = require("express"); const { User } = require("../models"); const TwoFactorService = require("../services/TwoFactorService"); const emailServices = require("../services/email"); const logger = require("../utils/logger"); const { authenticateToken } = require("../middleware/auth"); const { requireStepUpAuth } = require("../middleware/stepUpAuth"); const { csrfProtection } = require("../middleware/csrf"); const { sanitizeInput, validateTotpCode, validateEmailOtp, validateRecoveryCode, } = require("../middleware/validation"); const { twoFactorVerificationLimiter, twoFactorSetupLimiter, recoveryCodeLimiter, emailOtpSendLimiter, } = require("../middleware/rateLimiter"); const router = express.Router(); // Helper for structured security audit logging const auditLog = (req, action, userId, details = {}) => { logger.info({ type: 'security_audit', action, userId, ip: req.ip, userAgent: req.get('User-Agent'), ...details, }); }; // All routes require authentication router.use(authenticateToken); // ============================================ // SETUP ENDPOINTS // ============================================ /** * POST /api/2fa/setup/totp/init * Initialize TOTP setup - generate secret and QR code */ router.post( "/setup/totp/init", twoFactorSetupLimiter, csrfProtection, async (req, res) => { try { const user = await User.findByPk(req.user.id); if (!user) { return res.status(404).json({ error: "User not found" }); } if (user.twoFactorEnabled) { return res.status(400).json({ error: "Multi-factor authentication is already enabled", }); } // Generate TOTP secret and QR code const { qrCodeDataUrl, encryptedSecret, encryptedSecretIv } = await TwoFactorService.generateTotpSecret(user.email); // Store pending secret for verification await user.storePendingTotpSecret(encryptedSecret, encryptedSecretIv); auditLog(req, '2fa.setup.initiated', user.id, { method: 'totp' }); res.json({ qrCodeDataUrl, message: "Scan the QR code with your authenticator app", }); } catch (error) { logger.error("TOTP setup init error:", error); res.status(500).json({ error: "Failed to initialize TOTP setup" }); } } ); /** * POST /api/2fa/setup/totp/verify * Verify TOTP code and enable 2FA */ router.post( "/setup/totp/verify", twoFactorSetupLimiter, csrfProtection, sanitizeInput, validateTotpCode, async (req, res) => { try { const { code } = req.body; const user = await User.findByPk(req.user.id); if (!user) { return res.status(404).json({ error: "User not found" }); } if (user.twoFactorEnabled) { return res.status(400).json({ error: "Multi-factor authentication is already enabled", }); } if (!user.twoFactorSetupPendingSecret) { return res.status(400).json({ error: "No pending TOTP setup. Please start the setup process again.", }); } // Verify the code against the pending secret const isValid = user.verifyPendingTotpCode(code); if (!isValid) { return res.status(400).json({ error: "Invalid verification code. Please try again.", }); } // Generate recovery codes const { codes: recoveryCodes } = await TwoFactorService.generateRecoveryCodes(); // Enable TOTP await user.enableTotp(recoveryCodes); // Send confirmation email try { await emailServices.auth.sendTwoFactorEnabledEmail(user); } catch (emailError) { logger.error("Failed to send 2FA enabled email:", emailError); // Don't fail the request if email fails } auditLog(req, '2fa.setup.completed', user.id, { method: 'totp' }); res.json({ message: "Multi-factor authentication enabled successfully", recoveryCodes, warning: "Save these recovery codes in a secure location. You will not be able to see them again.", }); } catch (error) { logger.error("TOTP setup verify error:", error); res.status(500).json({ error: "Failed to enable multi-factor authentication" }); } } ); /** * POST /api/2fa/setup/email/init * Initialize email 2FA setup - send verification code */ router.post( "/setup/email/init", twoFactorSetupLimiter, emailOtpSendLimiter, csrfProtection, async (req, res) => { try { const user = await User.findByPk(req.user.id); if (!user) { return res.status(404).json({ error: "User not found" }); } if (user.twoFactorEnabled) { return res.status(400).json({ error: "Multi-factor authentication is already enabled", }); } // Generate and send email OTP const otpCode = await user.generateEmailOtp(); try { await emailServices.auth.sendTwoFactorOtpEmail(user, otpCode); } catch (emailError) { logger.error("Failed to send 2FA setup OTP email:", emailError); return res.status(500).json({ error: "Failed to send verification email" }); } auditLog(req, '2fa.setup.initiated', user.id, { method: 'email' }); res.json({ message: "Verification code sent to your email", }); } catch (error) { logger.error("Email 2FA setup init error:", error); res.status(500).json({ error: "Failed to initialize email 2FA setup" }); } } ); /** * POST /api/2fa/setup/email/verify * Verify email OTP and enable email 2FA */ router.post( "/setup/email/verify", twoFactorSetupLimiter, csrfProtection, sanitizeInput, validateEmailOtp, async (req, res) => { try { const { code } = req.body; const user = await User.findByPk(req.user.id); if (!user) { return res.status(404).json({ error: "User not found" }); } if (user.twoFactorEnabled) { return res.status(400).json({ error: "Multi-factor authentication is already enabled", }); } if (user.isEmailOtpLocked()) { return res.status(429).json({ error: "Too many failed attempts. Please request a new code.", }); } // Verify the OTP const isValid = user.verifyEmailOtp(code); if (!isValid) { await user.incrementEmailOtpAttempts(); return res.status(400).json({ error: "Invalid or expired verification code", }); } // Generate recovery codes const { codes: recoveryCodes } = await TwoFactorService.generateRecoveryCodes(); // Enable email 2FA await user.enableEmailTwoFactor(recoveryCodes); await user.clearEmailOtp(); // Send confirmation email try { await emailServices.auth.sendTwoFactorEnabledEmail(user); } catch (emailError) { logger.error("Failed to send 2FA enabled email:", emailError); } auditLog(req, '2fa.setup.completed', user.id, { method: 'email' }); res.json({ message: "Multi-factor authentication enabled successfully", recoveryCodes, warning: "Save these recovery codes in a secure location. You will not be able to see them again.", }); } catch (error) { logger.error("Email 2FA setup verify error:", error); res.status(500).json({ error: "Failed to enable multi-factor authentication" }); } } ); // ============================================ // VERIFICATION ENDPOINTS (Step-up auth) // ============================================ /** * POST /api/2fa/verify/totp * Verify TOTP code for step-up authentication */ router.post( "/verify/totp", twoFactorVerificationLimiter, csrfProtection, sanitizeInput, validateTotpCode, async (req, res) => { try { const { code } = req.body; const user = await User.findByPk(req.user.id); if (!user) { return res.status(404).json({ error: "User not found" }); } if (!user.twoFactorEnabled || user.twoFactorMethod !== "totp") { logger.warn(`2FA verify failed for user ${user.id}: TOTP not enabled or wrong method`); return res.status(400).json({ error: "Verification failed", }); } const isValid = user.verifyTotpCode(code); if (!isValid) { auditLog(req, '2fa.verify.failed', user.id, { method: 'totp' }); return res.status(400).json({ error: "Invalid verification code", }); } // Mark code as used for replay protection await user.markTotpCodeUsed(code); // Update step-up session await user.updateStepUpSession(); auditLog(req, '2fa.verify.success', user.id, { method: 'totp' }); res.json({ message: "Verification successful", verified: true, }); } catch (error) { logger.error("TOTP verification error:", error); res.status(500).json({ error: "Verification failed" }); } } ); /** * POST /api/2fa/verify/email/send * Send email OTP for step-up authentication */ router.post( "/verify/email/send", emailOtpSendLimiter, csrfProtection, async (req, res) => { try { const user = await User.findByPk(req.user.id); if (!user) { return res.status(404).json({ error: "User not found" }); } if (!user.twoFactorEnabled) { logger.warn(`2FA verify failed for user ${user.id}: 2FA not enabled`); return res.status(400).json({ error: "Verification failed", }); } // Generate and send email OTP const otpCode = await user.generateEmailOtp(); try { await emailServices.auth.sendTwoFactorOtpEmail(user, otpCode); } catch (emailError) { logger.error("Failed to send 2FA OTP email:", emailError); return res.status(500).json({ error: "Failed to send verification email" }); } auditLog(req, '2fa.otp.sent', user.id, { method: 'email' }); res.json({ message: "Verification code sent to your email", }); } catch (error) { logger.error("Email OTP send error:", error); res.status(500).json({ error: "Failed to send verification code" }); } } ); /** * POST /api/2fa/verify/email * Verify email OTP for step-up authentication */ router.post( "/verify/email", twoFactorVerificationLimiter, csrfProtection, sanitizeInput, validateEmailOtp, async (req, res) => { try { const { code } = req.body; const user = await User.findByPk(req.user.id); if (!user) { return res.status(404).json({ error: "User not found" }); } if (!user.twoFactorEnabled) { logger.warn(`2FA verify failed for user ${user.id}: 2FA not enabled`); return res.status(400).json({ error: "Verification failed", }); } if (user.isEmailOtpLocked()) { return res.status(429).json({ error: "Too many failed attempts. Please request a new code.", }); } const isValid = user.verifyEmailOtp(code); if (!isValid) { await user.incrementEmailOtpAttempts(); auditLog(req, '2fa.verify.failed', user.id, { method: 'email' }); return res.status(400).json({ error: "Invalid or expired verification code", }); } // Update step-up session and clear OTP await user.updateStepUpSession(); await user.clearEmailOtp(); auditLog(req, '2fa.verify.success', user.id, { method: 'email' }); res.json({ message: "Verification successful", verified: true, }); } catch (error) { logger.error("Email OTP verification error:", error); res.status(500).json({ error: "Verification failed" }); } } ); /** * POST /api/2fa/verify/recovery * Use recovery code for step-up authentication */ router.post( "/verify/recovery", recoveryCodeLimiter, csrfProtection, sanitizeInput, validateRecoveryCode, async (req, res) => { try { const { code } = req.body; const user = await User.findByPk(req.user.id); if (!user) { return res.status(404).json({ error: "User not found" }); } if (!user.twoFactorEnabled) { logger.warn(`2FA verify failed for user ${user.id}: 2FA not enabled`); return res.status(400).json({ error: "Verification failed", }); } const { valid, remainingCodes } = await user.useRecoveryCode(code); if (!valid) { auditLog(req, '2fa.verify.failed', user.id, { method: 'recovery' }); return res.status(400).json({ error: "Invalid recovery code", }); } // Send alert email about recovery code usage try { await emailServices.auth.sendRecoveryCodeUsedEmail(user, remainingCodes); } catch (emailError) { logger.error("Failed to send recovery code used email:", emailError); } auditLog(req, '2fa.recovery.used', user.id, { lowCodes: remainingCodes <= 2 }); res.json({ message: "Verification successful", verified: true, remainingCodes, warning: remainingCodes <= 2 ? "You are running low on recovery codes. Please generate new ones." : null, }); } catch (error) { logger.error("Recovery code verification error:", error); res.status(500).json({ error: "Verification failed" }); } } ); // ============================================ // MANAGEMENT ENDPOINTS // ============================================ /** * GET /api/2fa/status * Get current 2FA status for the user */ router.get("/status", async (req, res) => { try { const user = await User.findByPk(req.user.id); if (!user) { return res.status(404).json({ error: "User not found" }); } res.json({ enabled: user.twoFactorEnabled, method: user.twoFactorMethod, hasRecoveryCodes: user.getRemainingRecoveryCodes() > 0, lowRecoveryCodes: user.getRemainingRecoveryCodes() <= 2, }); } catch (error) { logger.error("2FA status error:", error); res.status(500).json({ error: "Failed to get 2FA status" }); } }); /** * POST /api/2fa/disable * Disable 2FA (requires step-up authentication) */ router.post( "/disable", csrfProtection, requireStepUpAuth("2fa_disable"), async (req, res) => { try { const user = await User.findByPk(req.user.id); if (!user) { return res.status(404).json({ error: "User not found" }); } if (!user.twoFactorEnabled) { logger.warn(`2FA disable failed for user ${user.id}: 2FA not enabled`); return res.status(400).json({ error: "Operation failed", }); } await user.disableTwoFactor(); // Send notification email try { await emailServices.auth.sendTwoFactorDisabledEmail(user); } catch (emailError) { logger.error("Failed to send 2FA disabled email:", emailError); } auditLog(req, '2fa.disabled', user.id); res.json({ message: "Multi-factor authentication has been disabled", }); } catch (error) { logger.error("2FA disable error:", error); res.status(500).json({ error: "Failed to disable multi-factor authentication" }); } } ); /** * POST /api/2fa/recovery/regenerate * Generate new recovery codes (requires step-up authentication) */ router.post( "/recovery/regenerate", csrfProtection, requireStepUpAuth("recovery_regenerate"), async (req, res) => { try { const user = await User.findByPk(req.user.id); if (!user) { return res.status(404).json({ error: "User not found" }); } if (!user.twoFactorEnabled) { logger.warn(`Recovery regenerate failed for user ${user.id}: 2FA not enabled`); return res.status(400).json({ error: "Operation failed", }); } const recoveryCodes = await user.regenerateRecoveryCodes(); auditLog(req, '2fa.recovery.regenerated', user.id); res.json({ recoveryCodes, warning: "Save these recovery codes in a secure location. Your previous codes are now invalid.", }); } catch (error) { logger.error("Recovery code regeneration error:", error); res.status(500).json({ error: "Failed to regenerate recovery codes" }); } } ); /** * GET /api/2fa/recovery/remaining * Get recovery codes status (not exact count for security) */ router.get("/recovery/remaining", async (req, res) => { try { const user = await User.findByPk(req.user.id); if (!user) { return res.status(404).json({ error: "User not found" }); } const remaining = user.getRemainingRecoveryCodes(); res.json({ hasRecoveryCodes: remaining > 0, lowRecoveryCodes: remaining <= 2, }); } catch (error) { logger.error("Recovery codes remaining error:", error); res.status(500).json({ error: "Failed to get recovery codes status" }); } }); module.exports = router;