Cookie-session auth (same-origin) #30

Closed
opened 2026-06-14 23:39:29 +02:00 by myrmidex · 2 comments
Owner

Same-origin cookie-session authentication via Symfony security (no JWT — no mobile/3rd-party clients planned).

  • User entity + Doctrine migration.
  • security.yaml: form-login/authenticator, HttpOnly + Secure + SameSite=Lax session cookie.
  • Login / logout / me endpoints under /api.
  • CSRF protection on login/state-changing endpoints.

Exit: can log in/out via the API; authenticated requests carry the session cookie; covered by functional tests.

Same-origin cookie-session authentication via Symfony security (no JWT — no mobile/3rd-party clients planned). - User entity + Doctrine migration. - `security.yaml`: form-login/authenticator, HttpOnly + Secure + SameSite=Lax session cookie. - Login / logout / `me` endpoints under `/api`. - CSRF protection on login/state-changing endpoints. Exit: can log in/out via the API; authenticated requests carry the session cookie; covered by functional tests.
myrmidex added this to the v0.3.0 milestone 2026-06-14 23:39:29 +02:00
myrmidex added the
enhancement
label 2026-06-14 23:39:29 +02:00
myrmidex self-assigned this 2026-06-14 23:39:29 +02:00
Author
Owner

CSRF scope clarification (2026-06-18)

The original plan was to add stateless double-submit CSRF tokens on /api/login via json_login's enable_csrf option. While implementing, we verified against the JsonLoginFactory/AbstractFactory source that json_login has no enable_csrf option — it's a form_login/logout-only key. /api/login is owned entirely by the firewall's JsonLoginAuthenticator (no controller in front), so there's no natural CSRF-token attachment point.

Cross-checked the current (2026) recommended approach for a same-origin React SPA + Symfony cookie-session stack (SymfonyCasts "React with Symfony" CSRF course + Symfony docs). The mainstream pattern does not put a CSRF token on the login endpoint; it protects login (and all mutating requests) with:

  1. SameSite=Lax session cookie (+ HttpOnly + Secure),
  2. Same-origin policy (no cross-domain AJAX), and
  3. Content-Type: application/json enforcement on mutating requests — a cross-site form/<img> CSRF can't set that header without a CORS preflight, which same-origin policy blocks. json_login already requires a JSON body, so this is free.

Resolution for this ticket: login's CSRF defense = SameSite=Lax + HttpOnly + Secure + JSON-Content-Type. No CSRF token on login. Explicit stateless double-submit CSRF is deferred to the first ticket introducing a mutating resource endpoint, where API Platform controllers provide a proper #[IsCsrfTokenValid] attachment point.

This narrows #30's CSRF scope but matches the current recommended pattern for this architecture. Exit criteria otherwise unchanged.

**CSRF scope clarification (2026-06-18)** The original plan was to add stateless double-submit CSRF tokens on `/api/login` via `json_login`'s `enable_csrf` option. While implementing, we verified against the `JsonLoginFactory`/`AbstractFactory` source that **`json_login` has no `enable_csrf` option** — it's a `form_login`/`logout`-only key. `/api/login` is owned entirely by the firewall's `JsonLoginAuthenticator` (no controller in front), so there's no natural CSRF-token attachment point. Cross-checked the current (2026) recommended approach for a **same-origin React SPA + Symfony cookie-session** stack (SymfonyCasts "React with Symfony" CSRF course + Symfony docs). The mainstream pattern does **not** put a CSRF token on the login endpoint; it protects login (and all mutating requests) with: 1. **`SameSite=Lax`** session cookie (+ HttpOnly + Secure), 2. **Same-origin policy** (no cross-domain AJAX), and 3. **`Content-Type: application/json` enforcement** on mutating requests — a cross-site form/`<img>` CSRF can't set that header without a CORS preflight, which same-origin policy blocks. `json_login` already requires a JSON body, so this is free. **Resolution for this ticket:** login's CSRF defense = SameSite=Lax + HttpOnly + Secure + JSON-Content-Type. No CSRF token on login. Explicit stateless double-submit CSRF is **deferred to the first ticket introducing a mutating resource endpoint**, where API Platform controllers provide a proper `#[IsCsrfTokenValid]` attachment point. This narrows #30's CSRF scope but matches the current recommended pattern for this architecture. Exit criteria otherwise unchanged.
Author
Owner

Follow-up: deferred stateless CSRF is now WON'T-DO (superseded by SameSite)

This ticket deferred "explicit stateless double-submit CSRF" to the first mutating-resource ticket (#31). Revisited during #31 planning and decided not to build it at all for this app:

SameSite=Lax on the session cookie means the browser does not attach the cookie to cross-site state-changing requests — which is the CSRF vector — so a forged cross-site write arrives unauthenticated. Combined with API Platform requiring Content-Type: application/json on writes (a classic CSRF can't set that header cross-origin without a CORS preflight our same-origin policy blocks) and this being a same-origin-only SPA with no 3rd-party/mobile clients, explicit CSRF tokens defend a vector already fully blocked.

Instead, #31 adds a regression-guard test that mutating /api writes require application/json, documenting the protection. Explicit tokens would only become relevant if the app ever adopted SameSite=None (cross-site embedding) — which it deliberately won't. No CSRF-token ticket will be opened.

**Follow-up: deferred stateless CSRF is now WON'T-DO (superseded by SameSite)** This ticket deferred "explicit stateless double-submit CSRF" to the first mutating-resource ticket (#31). Revisited during #31 planning and decided **not to build it at all** for this app: `SameSite=Lax` on the session cookie means the browser does not attach the cookie to cross-site state-changing requests — which *is* the CSRF vector — so a forged cross-site write arrives unauthenticated. Combined with API Platform requiring `Content-Type: application/json` on writes (a classic CSRF can't set that header cross-origin without a CORS preflight our same-origin policy blocks) and this being a same-origin-only SPA with no 3rd-party/mobile clients, explicit CSRF tokens defend a vector already fully blocked. Instead, #31 adds a regression-guard test that mutating `/api` writes require `application/json`, documenting the protection. Explicit tokens would only become relevant if the app ever adopted `SameSite=None` (cross-site embedding) — which it deliberately won't. No CSRF-token ticket will be opened.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lvl0/buckets#30
No description provided.