# 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=<token>; Path=/; SameSite=lax
       │                        │
       │                        │  Send index.html
       │                        │  └─> getCsrfToken(req, res) called
       │                        │
       │  index.html + JS       │
       │<───────────────────────┘
       │
       │  Read cookie: document.cookie
       │  Extract token from bt_csrf_token=<token>
       │
       │  POST /api/bills
       │  x-csrf-token: <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

- **BillTracker SPA default:** `CSRF_HTTP_ONLY=false` — Cookie IS accessible via JavaScript so `client/api.js` can send the matching `x-csrf-token` header.
- **Do not set `CSRF_HTTP_ONLY=true` for this SPA** unless token delivery changes away from `document.cookie`; otherwise mutating requests will fail CSRF validation.

### 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)
