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:
Radosław Gierwiało
2025-11-19 20:16:05 +01:00
parent cbc970f60b
commit 44df50362a
10 changed files with 687 additions and 12 deletions

View File

@@ -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,