MFA
This commit is contained in:
@@ -28,8 +28,7 @@ const csrfProtection = (req, res, next) => {
|
||||
}
|
||||
|
||||
// Get token from header or body
|
||||
const token =
|
||||
req.headers["x-csrf-token"] || req.body.csrfToken || req.query.csrfToken;
|
||||
const token = req.headers["x-csrf-token"] || req.body.csrfToken;
|
||||
|
||||
// Get token from cookie
|
||||
const cookieToken = req.cookies && req.cookies["csrf-token"];
|
||||
|
||||
@@ -207,6 +207,57 @@ const authRateLimiters = {
|
||||
legacyHeaders: false,
|
||||
handler: createRateLimitHandler('general'),
|
||||
}),
|
||||
|
||||
// Two-Factor Authentication rate limiters
|
||||
twoFactorVerification: rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 10, // 10 verification attempts per 15 minutes
|
||||
message: {
|
||||
error: "Too many verification attempts. Please try again later.",
|
||||
retryAfter: 900,
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skipSuccessfulRequests: true,
|
||||
handler: createRateLimitHandler('twoFactorVerification'),
|
||||
}),
|
||||
|
||||
twoFactorSetup: rateLimit({
|
||||
windowMs: 60 * 60 * 1000, // 1 hour
|
||||
max: 5, // 5 setup attempts per hour
|
||||
message: {
|
||||
error: "Too many setup attempts. Please try again later.",
|
||||
retryAfter: 3600,
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
handler: createRateLimitHandler('twoFactorSetup'),
|
||||
}),
|
||||
|
||||
recoveryCode: rateLimit({
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 3, // 3 recovery code attempts per 15 minutes
|
||||
message: {
|
||||
error: "Too many recovery code attempts. Please try again later.",
|
||||
retryAfter: 900,
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
skipSuccessfulRequests: false, // Count all attempts for security
|
||||
handler: createRateLimitHandler('recoveryCode'),
|
||||
}),
|
||||
|
||||
emailOtpSend: rateLimit({
|
||||
windowMs: 10 * 60 * 1000, // 10 minutes
|
||||
max: 2, // 2 OTP sends per 10 minutes
|
||||
message: {
|
||||
error: "Please wait before requesting another code.",
|
||||
retryAfter: 600,
|
||||
},
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
handler: createRateLimitHandler('emailOtpSend'),
|
||||
}),
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
@@ -223,6 +274,12 @@ module.exports = {
|
||||
emailVerificationLimiter: authRateLimiters.emailVerification,
|
||||
generalLimiter: authRateLimiters.general,
|
||||
|
||||
// Two-Factor Authentication rate limiters
|
||||
twoFactorVerificationLimiter: authRateLimiters.twoFactorVerification,
|
||||
twoFactorSetupLimiter: authRateLimiters.twoFactorSetup,
|
||||
recoveryCodeLimiter: authRateLimiters.recoveryCode,
|
||||
emailOtpSendLimiter: authRateLimiters.emailOtpSend,
|
||||
|
||||
// Burst protection
|
||||
burstProtection,
|
||||
|
||||
|
||||
73
backend/middleware/stepUpAuth.js
Normal file
73
backend/middleware/stepUpAuth.js
Normal file
@@ -0,0 +1,73 @@
|
||||
const TwoFactorService = require("../services/TwoFactorService");
|
||||
const logger = require("../utils/logger");
|
||||
|
||||
/**
|
||||
* Middleware to require step-up authentication for sensitive actions.
|
||||
* Only applies to users who have 2FA enabled.
|
||||
*
|
||||
* @param {string} action - The sensitive action being protected
|
||||
* @returns {Function} Express middleware function
|
||||
*/
|
||||
const requireStepUpAuth = (action) => {
|
||||
return async (req, res, next) => {
|
||||
try {
|
||||
// If user doesn't have 2FA enabled, skip step-up requirement
|
||||
if (!req.user.twoFactorEnabled) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Check if user has a valid step-up session (within 5 minutes)
|
||||
const isValid = TwoFactorService.validateStepUpSession(req.user);
|
||||
|
||||
if (!isValid) {
|
||||
logger.info(
|
||||
`Step-up authentication required for user ${req.user.id}, action: ${action}`
|
||||
);
|
||||
|
||||
return res.status(403).json({
|
||||
error: "Multi-factor authentication required",
|
||||
code: "STEP_UP_REQUIRED",
|
||||
action: action,
|
||||
methods: getTwoFactorMethods(req.user),
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
logger.error("Step-up auth middleware error:", error);
|
||||
return res.status(500).json({
|
||||
error: "An error occurred during authentication",
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get available 2FA methods for a user
|
||||
* @param {Object} user - User object
|
||||
* @returns {string[]} Array of available methods
|
||||
*/
|
||||
function getTwoFactorMethods(user) {
|
||||
const methods = [];
|
||||
|
||||
// Primary method is always available
|
||||
if (user.twoFactorMethod === "totp") {
|
||||
methods.push("totp");
|
||||
}
|
||||
|
||||
// Email is always available as a backup method
|
||||
methods.push("email");
|
||||
|
||||
// Recovery codes are available if any remain
|
||||
if (user.recoveryCodesHash) {
|
||||
const recoveryData = JSON.parse(user.recoveryCodesHash);
|
||||
const remaining = TwoFactorService.getRemainingRecoveryCodesCount(recoveryData);
|
||||
if (remaining > 0) {
|
||||
methods.push("recovery");
|
||||
}
|
||||
}
|
||||
|
||||
return methods;
|
||||
}
|
||||
|
||||
module.exports = { requireStepUpAuth };
|
||||
@@ -345,6 +345,31 @@ const validateCoordinatesBody = [
|
||||
.withMessage("Longitude must be between -180 and 180"),
|
||||
];
|
||||
|
||||
// Two-Factor Authentication validation
|
||||
const validateTotpCode = [
|
||||
body("code")
|
||||
.trim()
|
||||
.matches(/^\d{6}$/)
|
||||
.withMessage("TOTP code must be exactly 6 digits"),
|
||||
handleValidationErrors,
|
||||
];
|
||||
|
||||
const validateEmailOtp = [
|
||||
body("code")
|
||||
.trim()
|
||||
.matches(/^\d{6}$/)
|
||||
.withMessage("Email OTP must be exactly 6 digits"),
|
||||
handleValidationErrors,
|
||||
];
|
||||
|
||||
const validateRecoveryCode = [
|
||||
body("code")
|
||||
.trim()
|
||||
.matches(/^[A-Za-z0-9]{4}-[A-Za-z0-9]{4}$/i)
|
||||
.withMessage("Recovery code must be in format XXXX-XXXX"),
|
||||
handleValidationErrors,
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
sanitizeInput,
|
||||
handleValidationErrors,
|
||||
@@ -359,4 +384,8 @@ module.exports = {
|
||||
validateFeedback,
|
||||
validateCoordinatesQuery,
|
||||
validateCoordinatesBody,
|
||||
// Two-Factor Authentication
|
||||
validateTotpCode,
|
||||
validateEmailOtp,
|
||||
validateRecoveryCode,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user