# CSRF Protection Design ## Problem All 8 session-authenticated state-changing endpoints are unprotected against CSRF. The `SameSite=Lax` session cookie default provides partial browser-level mitigation but OWASP classifies it as defense-in-depth only, not a primary defense. ### Vulnerable Endpoints | Endpoint | Input Type | Risk | |----------|-----------|------| | `POST /login/password` | htmx form | Login CSRF | | `POST /logout` | no body | Forced logout | | `POST /manage/credentials/password` | htmx form | Password change | | `DELETE /manage/credentials/password` | htmx | Credential removal | | `POST /manage/credentials/webauthn/begin` | JSON | Challenge generation | | `POST /manage/credentials/webauthn/complete` | JSON | Credential registration | | `DELETE /manage/credentials/webauthn/{id}` | htmx | Credential removal | | `POST /manage/profile` | htmx form | Profile modification | | `POST /consent` | native form | Auto-approve consent | ### Exempt Endpoints | Endpoint | Reason | |----------|--------| | `POST /token` | Client-authenticated (client_secret_basic), not session-based | | `POST /userinfo` | Bearer token auth, not session-based | | `GET/POST /.well-known/*` | Public discovery, no state changes | | All GET/HEAD/OPTIONS | Safe methods | ## Approach: Synchronizer Token Pattern OWASP's primary recommendation for stateful applications. We already have server-side sessions via Starlette's `SessionMiddleware`. References: - [OWASP CSRF Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html) - [OWASP Synchronizer Token Pattern](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern) ## Design ### Token Lifecycle - **Generation**: `secrets.token_urlsafe(32)`, stored at `request.session["csrf_token"]`. Generated lazily on first access (when a template renders or middleware checks). - **Scope**: One token per session (not per request). OWASP notes per-request tokens hurt usability (Back button, parallel tabs) with no meaningful security gain. - **Rotation**: Token regenerated on login and logout (session fixation defense). ### CSRF Middleware ASGI middleware (`CSRFMiddleware`) added after `SessionMiddleware` in the stack: 1. **Skip safe methods**: GET, HEAD, OPTIONS pass through. 2. **Skip exempt paths**: `/token`, `/userinfo` (Bearer/client auth, not sessions). 3. **For POST/DELETE/PUT/PATCH**: - Read expected token from `request.session["csrf_token"]` - Read submitted token from: - Form field `csrf_token` (form submissions) - Header `X-CSRF-Token` (JSON/htmx requests) - Compare using `hmac.compare_digest()` (constant-time) - Missing or mismatch: 403 Forbidden 4. **Defense-in-depth Origin check**: If `Origin` header present, verify it matches the configured server origin. If absent, fall back to `Referer`. If neither is present, rely on the token check (some privacy setups strip both headers). ### Template Integration - **Jinja2 context processor**: `csrf_token` available in all templates via template globals. Reads from session, generating token if absent. - **Hidden form field**: All 4 form templates get ``: - `login.html` - `consent.html` - `manage/credentials.html` - `manage/profile.html` - **htmx header injection**: `base.html` gets `hx-headers='{"X-CSRF-Token": "{{ csrf_token }}"}'` on `
`. All htmx requests automatically include the token as a custom header. - **WebAuthn JS**: `webauthn.js` fetch calls read the token from a `` tag in `base.html` and include it as `X-CSRF-Token` header. ### Session Cookie Hardening Harden `SessionMiddleware` configuration in `app.py`: - `same_site="lax"` -- explicit instead of relying on Starlette default - `https_only` -- tied to new config `session_https_only: bool = True`. Override for local dev via `OIDC_OP_SESSION_HTTPS_ONLY=false`. ### Error Handling - CSRF failures return 403 Forbidden with a plain HTML error page. - Logged at WARNING level with the request path. - No redirect loops. ## Out of Scope - CORS configuration for OIDC endpoints (separate concern, tracked separately). - XSS prevention (CSRF protection assumes no XSS -- Jinja2 auto-escaping is already in place).