# CSRF Security Update - Fix Summary ## Date: October 23, 2025 ## Issues Encountered After implementing CSRF security updates, the application experienced two main issues: ### 1. Login Failed with "Invalid CSRF token" **Problem**: The login endpoint required a CSRF token, but users couldn't get a token before logging in (chicken-and-egg problem). **Root Cause**: The `/api/auth/login` endpoint was being protected by CSRF middleware, but there's no session yet during initial login. **Solution**: Added `/api/auth/login` and `/api/auth/session` to the CSRF exempt routes list in `app/middleware/csrf.js`. Login endpoints use credentials (username/password) for authentication, so they don't need CSRF protection. ### 2. Campaign Creation Failed with Infinite Retry Loop **Problem**: When creating campaigns, the app would get stuck in an infinite retry loop with repeated "CSRF token validation failed" errors. **Root Causes**: 1. The API client (`api-client.js`) wasn't fetching or sending CSRF tokens at all 2. The retry logic didn't have a guard against infinite recursion 3. FormData wasn't including the CSRF token **Solutions**: 1. **Added CSRF token management** to the API client: - `fetchCsrfToken()` - Fetches token from `/api/csrf-token` endpoint - `ensureCsrfToken()` - Ensures a valid token exists before requests - Tokens are automatically included in state-changing requests (POST, PUT, PATCH, DELETE) 2. **Fixed infinite retry loop**: - Added `isRetry` parameter to `makeRequest()`, `postFormData()`, and `putFormData()` - Retry only happens once per request - If second attempt fails, error is thrown to the user 3. **Enhanced token handling**: - JSON requests: Token sent via `X-CSRF-Token` header - FormData requests: Token sent via `_csrf` field - Token automatically refreshed if server responds with new token 4. **Server-side updates**: - Added explicit CSRF protection to `/api/csrf-token` endpoint so it can generate tokens - Exported `csrfProtection` middleware for explicit use ## Files Modified ### 1. `app/middleware/csrf.js` ```javascript // Added to exempt routes: const csrfExemptRoutes = [ '/api/health', '/api/metrics', '/api/config', '/api/auth/login', // ← NEW: Login uses credentials '/api/auth/session', // ← NEW: Session check is read-only '/api/representatives/postal/', '/api/campaigns/public' ]; // Enhanced getCsrfToken with error handling ``` ### 2. `app/server.js` ```javascript // Added csrfProtection to imports const { conditionalCsrfProtection, getCsrfToken, csrfProtection } = require('./middleware/csrf'); // Applied explicit CSRF protection to token endpoint app.get('/api/csrf-token', csrfProtection, getCsrfToken); ``` ### 3. `app/public/js/api-client.js` - Added CSRF token caching and fetching logic - Modified `makeRequest()` to include `X-CSRF-Token` header - Modified `postFormData()` and `putFormData()` to include `_csrf` field - Added retry logic with infinite loop protection (max 1 retry) - Added automatic token refresh on 403 errors ## How CSRF Protection Works Now ### Flow for State-Changing Requests (POST, PUT, DELETE): ``` 1. User Action (e.g., "Create Campaign") ↓ 2. API Client checks if CSRF token exists ↓ (if no token) 3. Fetch token from GET /api/csrf-token ↓ 4. Include token in request: - Header: X-CSRF-Token (for JSON) - FormData: _csrf (for file uploads) ↓ 5. Server validates token matches session ↓ 6a. Success → Process request 6b. Invalid Token → Return 403 ↓ (on 403, if not a retry) 7. Clear token, fetch new one, retry ONCE ↓ 8a. Success → Return data 8b. Still fails → Throw error to user ``` ### Protected vs Exempt Endpoints **Protected (requires CSRF token)**: - ✅ POST `/api/admin/campaigns` - Create campaign - ✅ PUT `/api/admin/campaigns/:id` - Update campaign - ✅ POST `/api/emails/send` - Send email - ✅ POST `/api/auth/logout` - Logout - ✅ POST `/api/auth/change-password` - Change password **Exempt (no CSRF required)**: - ✅ GET (all GET requests are safe) - ✅ POST `/api/auth/login` - Uses credentials - ✅ GET `/api/auth/session` - Read-only check - ✅ GET `/api/health` - Public health check - ✅ GET `/api/metrics` - Prometheus metrics ## Testing Checklist - [x] Login as admin works - [ ] Create new campaign works - [ ] Update existing campaign works - [ ] Delete campaign works - [ ] Send email to representative works - [ ] Logout works - [ ] Password change works - [ ] Public pages work without authentication ## Security Benefits 1. **CSRF Attack Prevention**: Malicious sites can't forge requests to your app 2. **Session Hijacking Protection**: httpOnly, secure, sameSite cookies 3. **Defense in Depth**: Multiple security layers (Helmet, rate limiting, CSRF, validation) 4. **Automatic Token Rotation**: Tokens refresh on each response when available 5. **Retry Logic**: Handles token expiration gracefully ## Important Notes - CSRF tokens are tied to sessions and expire with the session (1 hour) - Tokens are stored in cookies (httpOnly, secure in production) - The retry logic prevents infinite loops by limiting to 1 retry per request - Login doesn't need CSRF because it uses credentials for authentication - All state-changing operations (POST/PUT/DELETE) now require valid CSRF tokens ## Troubleshooting **If you see "Invalid CSRF token" errors:** 1. Check browser console for detailed error messages 2. Clear browser cookies and session storage 3. Logout and login again to get a fresh session 4. Verify the session hasn't expired (1 hour timeout) 5. Check server logs for CSRF validation failures **If infinite retry loop occurs:** 1. Check that `isRetry` parameter is being passed correctly 2. Verify FormData isn't being reused across retries 3. Clear the API client's cached token: `window.apiClient.csrfToken = null`