# CSRF SPA Setup Guide ## Overview This document describes the CSRF (Cross-Site Request Forgery) protection implementation for the Single Page Application (SPA) frontend. ## Problem Statement ### Symptoms - SPA (Single Page Application) couldn't read the CSRF cookie - Create user and other POST requests failed with: `"Your session has expired or this request may be fraudulent"` - The CSRF cookie was set as HttpOnly by default, preventing JavaScript access ### Root Causes 1. The `csrfTokenProvider` middleware sets `res.locals.csrfToken` but the CSRF cookie wasn't being generated for SPA `index.html` requests 2. The `*` route serving `index.html` was bypassing CSRF cookie generation 3. HttpOnly cookies cannot be read by JavaScript, which prevented the SPA from extracting the token ## Solution ### Implementation in `server.js` The fix ensures the CSRF cookie is set before sending the SPA `index.html`: ```javascript const { getCsrfToken } = require('./middleware/csrf'); app.get('*', (req, res) => { // Set CSRF cookie if not present (needed for SPA to read token) getCsrfToken(req, res); res.sendFile(path.join(DIST, 'index.html')); }); ``` ### Environment Configuration Add these environment variables to your `.env` file: ```bash # CSRF Settings for SPA CSRF_HTTP_ONLY=false # Allow JavaScript to read cookie (SPA mode) CSRF_SAME_SITE=lax # Better SPA compatibility (vs 'strict') CSRF_COOKIE_NAME=bt_csrf_token # Customize cookie name if needed ``` > **Note:** When `CSRF_HTTP_ONLY=false`, the cookie is accessible via JavaScript (`document.cookie`). This is required for SPA CSRF token extraction but should only be used when the application is served from the same origin. ## How It Works ### Double-Submit Pattern The implementation uses the **double-submit cookie pattern**: 1. **Server** sets a CSRF cookie with a cryptographically secure token 2. **Client** reads the cookie via JavaScript and sends it in the `x-csrf-token` header for state-changing requests 3. **Server** validates that the header token matches the cookie token ### Flow Diagram ``` ┌─────────────┐ ┌─────────────┐ │ Browser │ │ Server │ └──────┬──────┘ └──────┬──────┘ │ │ │ GET / │ │───────────────────────>│ │ │ │ │ Set CSRF cookie (httpOnly=false) │ │ Set-Cookie: bt_csrf_token=; Path=/; SameSite=lax │ │ │ │ Send index.html │ │ └─> getCsrfToken(req, res) called │ │ │ index.html + JS │ │<───────────────────────┘ │ │ Read cookie: document.cookie │ Extract token from bt_csrf_token= │ │ POST /api/bills │ x-csrf-token: │───────────────────────> │ │ │ │ Validate: header token == cookie token │ │ If match → process request │ │ If mismatch → 403 Forbidden │ │ │ 200 OK / 403 │ │<───────────────────────┘ ``` ### Code Flow 1. **Request enters `server.js`** - `csrfTokenProvider` middleware runs on every request (via `app.use(csrfTokenProvider)`) - This ensures `res.locals.csrfToken` is always available 2. **SPA route `app.get('*')` is hit** - Explicitly calls `getCsrfToken(req, res)` before sending `index.html` - This guarantees the CSRF cookie is set even for the initial SPA load 3. **Frontend reads the cookie** ```javascript // Example from frontend function getCsrfToken() { const match = document.cookie.match(/bt_csrf_token=([^;]+)/); return match ? match[1] : null; } ``` 4. **API calls include the token** ```javascript const token = getCsrfToken(); fetch('/api/bills', { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-csrf-token': token, }, body: JSON.stringify(data), }); ``` 5. **Server validates** - `csrfMiddleware` extracts the token from: - `x-csrf-token` header (preferred for API) - `csrf_token` query parameter (form submissions) - `csrf_token` body field (form submissions) - Compares against the cookie token - Returns 403 if validation fails ## Security Considerations ### HttpOnly Setting - **Default (Secure):** `CSRF_HTTP_ONLY=true` — Cookie is NOT accessible via JavaScript, only sent automatically with requests - **SPA Mode:** `CSRF_HTTP_ONLY=false` — Cookie IS accessible via JavaScript, enabling the double-submit pattern ### SameSite Attribute - `lax` (recommended for SPA): Allows cookie to be sent with top-level navigations but blocks cross-site POST requests - `strict`: Most secure but may break SPA functionality across domains - `none`: Requires `Secure=true` (HTTPS only) ### Cookie Name Default: `bt_csrf_token` Customize via `CSRF_COOKIE_NAME` if running multiple applications on the same domain. ## Troubleshooting ### Issue: "CSRF token validation failed" on SPA **Symptoms:** - POST/PUT/DELETE/PATCH requests fail with 403 - Error message: "Your session has expired or this request may be fraudulent" **Check:** 1. Verify `CSRF_HTTP_ONLY=false` is set 2. Check browser DevTools → Application → Cookies → Ensure `bt_csrf_token` cookie exists 3. Verify the frontend is sending `x-csrf-token` header with correct token value ### Issue: Cookie not being set **Symptoms:** - No `bt_csrf_token` cookie appears in browser DevTools **Check:** 1. Verify `app.use(csrfTokenProvider)` is in `server.js` 2. Ensure `getCsrfToken(req, res)` is called in the SPA route handler 3. Check that cookies are not blocked by browser settings or extensions ### Issue: Token mismatch **Symptoms:** - Cookie exists but validation still fails **Check:** 1. Clear browser cookies and refresh 2. Ensure only one CSRF cookie exists (no duplicate names) 3. Check server restart didn't generate a new cookie before the SPA read the old one ## References - [`middleware/csrf.js`](../middleware/csrf.js) — Core CSRF implementation - [`server.js`](../server.js) — Express server with SPA route - [OWASP CSRF Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html)