191 lines
6.8 KiB
Markdown
191 lines
6.8 KiB
Markdown
# 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
|
|
|
|
- **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)
|