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:
@@ -11,11 +11,36 @@ class ApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
// CSRF Token Management (Phase 3 - Security Hardening)
|
||||
let csrfToken = null;
|
||||
|
||||
async function getCsrfToken() {
|
||||
if (csrfToken) return csrfToken;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/csrf-token`, {
|
||||
credentials: 'include', // Important for cookies
|
||||
});
|
||||
const data = await response.json();
|
||||
csrfToken = data.csrfToken;
|
||||
return csrfToken;
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch CSRF token:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset CSRF token (call this on 403 CSRF errors)
|
||||
function resetCsrfToken() {
|
||||
csrfToken = null;
|
||||
}
|
||||
|
||||
async function fetchAPI(endpoint, options = {}) {
|
||||
const url = `${API_URL}${endpoint}`;
|
||||
|
||||
const config = {
|
||||
...options,
|
||||
credentials: 'include', // Include cookies for CSRF
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
@@ -28,6 +53,14 @@ async function fetchAPI(endpoint, options = {}) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// Add CSRF token for state-changing requests (Phase 3)
|
||||
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(options.method?.toUpperCase())) {
|
||||
const csrf = await getCsrfToken();
|
||||
if (csrf) {
|
||||
config.headers['X-CSRF-Token'] = csrf;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, config);
|
||||
|
||||
@@ -44,6 +77,15 @@ async function fetchAPI(endpoint, options = {}) {
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
// Handle CSRF token errors (Phase 3)
|
||||
if (response.status === 403 && data.error === 'Invalid CSRF token') {
|
||||
resetCsrfToken();
|
||||
// Retry the request once with a fresh CSRF token
|
||||
if (!options._csrfRetry) {
|
||||
return fetchAPI(endpoint, { ...options, _csrfRetry: true });
|
||||
}
|
||||
}
|
||||
|
||||
throw new ApiError(
|
||||
data.error || 'API request failed',
|
||||
response.status,
|
||||
|
||||
Reference in New Issue
Block a user