feat(security): implement comprehensive security hardening
- Add CSRF protection with cookie-based tokens - Add cookie-parser and csurf middleware - Create GET /api/csrf-token endpoint - Frontend automatically includes CSRF token in POST/PUT/DELETE requests - Add retry logic for expired CSRF tokens - Implement account lockout mechanism - Add database fields: failedLoginAttempts, lockedUntil - Track failed login attempts and lock accounts after max attempts (configurable) - Auto-unlock after lockout duration expires - Return helpful error messages with remaining time - Add comprehensive security environment variables - Rate limiting configuration (API, auth, email endpoints) - CSRF protection toggle - Password policy requirements - Account lockout settings - Logging levels - Add comprehensive test coverage - 6 new tests for account lockout functionality - 11 new tests for CSRF protection - All tests handle enabled/disabled states gracefully - Update documentation - Add Phase 3 security hardening to SESSION_CONTEXT.md - Document new database fields and migration - Update progress to 85% Files changed: - Backend: app.js, auth controller, security config, new migration - Frontend: api.js with CSRF token handling - Tests: auth.test.js (extended), csrf.test.js (new) - Config: .env examples with security variables - Docs: SESSION_CONTEXT.md updated
This commit is contained in:
@@ -9,6 +9,7 @@ const {
|
||||
} = require('../utils/auth');
|
||||
const { sendVerificationEmail, sendWelcomeEmail, sendPasswordResetEmail } = require('../utils/email');
|
||||
const { sanitizeForEmail, timingSafeEqual } = require('../utils/sanitize');
|
||||
const securityConfig = require('../config/security');
|
||||
|
||||
// Register new user (Phase 1.5 - with WSDC support and email verification)
|
||||
async function register(req, res, next) {
|
||||
@@ -104,7 +105,7 @@ async function register(req, res, next) {
|
||||
}
|
||||
}
|
||||
|
||||
// Login user
|
||||
// Login user (with account lockout protection - Phase 3)
|
||||
async function login(req, res, next) {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
@@ -121,16 +122,77 @@ async function login(req, res, next) {
|
||||
});
|
||||
}
|
||||
|
||||
// Check if account is locked (Phase 3 - Account Lockout)
|
||||
if (securityConfig.accountLockout.enabled && user.lockedUntil && user.lockedUntil > new Date()) {
|
||||
const remainingMinutes = Math.ceil((user.lockedUntil - new Date()) / 60000);
|
||||
return res.status(423).json({
|
||||
success: false,
|
||||
error: 'Account is temporarily locked due to too many failed login attempts',
|
||||
message: `Account is locked. Please try again in ${remainingMinutes} minute(s).`,
|
||||
lockedUntil: user.lockedUntil,
|
||||
});
|
||||
}
|
||||
|
||||
// Compare password
|
||||
const isPasswordValid = await comparePassword(password, user.passwordHash);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
// Increment failed login attempts (Phase 3 - Account Lockout)
|
||||
if (securityConfig.accountLockout.enabled) {
|
||||
const newFailedAttempts = user.failedLoginAttempts + 1;
|
||||
const updateData = {
|
||||
failedLoginAttempts: newFailedAttempts,
|
||||
};
|
||||
|
||||
// Lock account if max attempts reached
|
||||
if (newFailedAttempts >= securityConfig.accountLockout.maxAttempts) {
|
||||
const lockoutDuration = securityConfig.accountLockout.lockoutDuration * 60 * 1000; // minutes to ms
|
||||
updateData.lockedUntil = new Date(Date.now() + lockoutDuration);
|
||||
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
return res.status(423).json({
|
||||
success: false,
|
||||
error: 'Account has been locked due to too many failed login attempts',
|
||||
message: `Too many failed login attempts. Account is locked for ${securityConfig.accountLockout.lockoutDuration} minutes.`,
|
||||
});
|
||||
}
|
||||
|
||||
// Update failed attempts count
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
const attemptsLeft = securityConfig.accountLockout.maxAttempts - newFailedAttempts;
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Invalid credentials',
|
||||
attemptsLeft: attemptsLeft > 0 ? attemptsLeft : 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Account lockout disabled - return generic error
|
||||
return res.status(401).json({
|
||||
success: false,
|
||||
error: 'Invalid credentials',
|
||||
});
|
||||
}
|
||||
|
||||
// Successful login - reset failed attempts and unlock account (Phase 3)
|
||||
if (securityConfig.accountLockout.enabled && (user.failedLoginAttempts > 0 || user.lockedUntil)) {
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
failedLoginAttempts: 0,
|
||||
lockedUntil: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Generate token
|
||||
const token = generateToken({ userId: user.id });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user