feat(auth): HttpOnly cookie sessions for the web UI #92

Merged
kreativmonkey merged 1 commit from feat/cookie-sessions into main 2026-06-14 14:48:20 +02:00
Owner

Behebt das „alle paar Stunden neu einloggen" in der PWA und schließt die localStorage-Token-Lücke — ohne den API-/MCP-Bearer-Flow anzutasten.

Problem

Nach OIDC stellte der Server ein rollendes __web_session-Token aus, das das SPA im localStorage hielt und als Authorization: Bearer sendete. Idle-Fenster nur 12 h, und ein Token im localStorage ist per XSS stehlbar.

Lösung

Web-Sessions laufen jetzt über ein HttpOnly; Secure; SameSite=Lax-Cookie (goloom_session). Kein Token mehr im localStorage; das Cookie ist per JS nicht auslesbar.

  • SESSION_TTL (Default 30 Tage), rollend bei jeder Nutzung — in beiden Stores (Rolling-Update + CreateSessionAPIToken) und der Cookie-Max-Age.
  • CSRF (stärkster Schutz, MCP bleibt nutzbar): RequireAuth akzeptiert das Cookie, wenn kein Bearer-Header da ist. Cookie-authentifizierte state-changing Requests brauchen ein Double-Submit-Token (goloom_csrf-Cookie + X-CSRF-Token-Header). Bearer-Requests (API-Tokens/MCP) sind CSRF-exempt und völlig unverändert.
  • OIDC-Callback setzt die Cookies und macht ein 303-Redirect (kein Token mehr in der URL/Hash).
  • Neu: POST /v1/auth/session/token (Token-/Recovery-Login → Cookie) und POST /v1/auth/logout (Cookies löschen + Session-Zeile widerrufen).
  • SPA: api-Client sendet credentials + CSRF-Header; „eingeloggt?" wird per /v1/me-Probe bestimmt (kein gespeichertes Token); Recovery-Login nutzt session/token; Logout ruft den Endpoint. Das Dev-„Browser session"-Panel behält seinen Bearer-Override.

Sicherheit

  • Cookie ist opak, gehasht gespeichert, rollend, widerrufbar (Token-Liste, „dieser Browser"), per Logout serverseitig gelöscht.
  • XSS kann das Session-Cookie nicht mehr auslesen (HttpOnly).
  • CSRF: SameSite=Lax + CORS-Allowlist + Double-Submit-Token.

Tests

  • Go: Cookie-Auth + CSRF inкл. Bearer-Bypass in RequireAuth; session/token+logout-Lebenszyklus (Cookie HttpOnly, Revoke wirkt); go build/vet/test ./... + make test-postgres grün.
  • Frontend: tsc/vite build + eslint (0 errors).
  • Playwright: neuer cookie-session.spec (HttpOnly-Cookie, kein localStorage-Token, CSRF-loser POST → 403); auth-settings „dieser Browser" auf der Web-Session-Zeile; volle Suite grün (1 bekannter Cross-Spec-Flake, isoliert grün).

⚠️ Ops

Produktiv HTTPS erforderlich (Secure-Cookie wird über http verworfen; für lokales app_env=development über http automatisch gelockert). SESSION_TTL per env konfigurierbar.

🤖 Generated with Claude Code

Behebt das „alle paar Stunden neu einloggen" in der PWA und schließt die localStorage-Token-Lücke — ohne den API-/MCP-Bearer-Flow anzutasten. ## Problem Nach OIDC stellte der Server ein rollendes `__web_session`-Token aus, das das SPA im **localStorage** hielt und als `Authorization: Bearer` sendete. Idle-Fenster nur 12 h, und ein Token im localStorage ist per XSS stehlbar. ## Lösung **Web-Sessions laufen jetzt über ein `HttpOnly; Secure; SameSite=Lax`-Cookie** (`goloom_session`). Kein Token mehr im localStorage; das Cookie ist per JS nicht auslesbar. - **`SESSION_TTL`** (Default **30 Tage**), rollend bei jeder Nutzung — in beiden Stores (Rolling-Update + `CreateSessionAPIToken`) und der Cookie-Max-Age. - **CSRF (stärkster Schutz, MCP bleibt nutzbar):** `RequireAuth` akzeptiert das Cookie, wenn kein Bearer-Header da ist. Cookie-authentifizierte **state-changing** Requests brauchen ein **Double-Submit-Token** (`goloom_csrf`-Cookie + `X-CSRF-Token`-Header). **Bearer-Requests (API-Tokens/MCP) sind CSRF-exempt** und völlig unverändert. - **OIDC-Callback** setzt die Cookies und macht ein `303`-Redirect (kein Token mehr in der URL/Hash). - Neu: **`POST /v1/auth/session/token`** (Token-/Recovery-Login → Cookie) und **`POST /v1/auth/logout`** (Cookies löschen + Session-Zeile widerrufen). - **SPA:** api-Client sendet `credentials` + CSRF-Header; „eingeloggt?" wird per `/v1/me`-Probe bestimmt (kein gespeichertes Token); Recovery-Login nutzt `session/token`; Logout ruft den Endpoint. Das Dev-„Browser session"-Panel behält seinen Bearer-Override. ## Sicherheit - Cookie ist opak, gehasht gespeichert, rollend, **widerrufbar** (Token-Liste, „dieser Browser"), per Logout serverseitig gelöscht. - XSS kann das Session-Cookie nicht mehr auslesen (HttpOnly). - CSRF: SameSite=Lax + CORS-Allowlist + Double-Submit-Token. ## Tests - Go: Cookie-Auth + CSRF inкл. **Bearer-Bypass** in `RequireAuth`; `session/token`+`logout`-Lebenszyklus (Cookie HttpOnly, Revoke wirkt); `go build/vet/test ./...` + `make test-postgres` grün. - Frontend: `tsc`/`vite build` + `eslint` (0 errors). - Playwright: neuer `cookie-session.spec` (HttpOnly-Cookie, kein localStorage-Token, CSRF-loser POST → 403); `auth-settings` „dieser Browser" auf der Web-Session-Zeile; volle Suite grün (1 bekannter Cross-Spec-Flake, isoliert grün). ## ⚠️ Ops **Produktiv HTTPS erforderlich** (Secure-Cookie wird über http verworfen; für lokales `app_env=development` über http automatisch gelockert). `SESSION_TTL` per env konfigurierbar. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(auth): HttpOnly cookie sessions for the web UI
All checks were successful
CI/CD Workflow / Test Backend (Go) (pull_request) Successful in 2m52s
CI/CD Workflow / Test Frontend (Node.js) (pull_request) Successful in 1m23s
CI/CD Workflow / Test Postgres Store (pull_request) Successful in 1m19s
CI/CD Workflow / E2E (Playwright + Go) (pull_request) Successful in 6m53s
CI/CD Workflow / Build and Publish Docker Image (pull_request) Has been skipped
CI/CD Workflow / Trigger Dockhand Deployment (pull_request) Has been skipped
02ecdc7cd1
Web sessions move from a bearer token in localStorage to an HttpOnly+Secure+
SameSite=Lax cookie, so the PWA stays signed in like a native app without
exposing a stealable token. API tokens / MCP are untouched (Authorization:
Bearer, inherently CSRF-safe).

- SESSION_TTL config (default 30d), rolling on use, threaded into both stores
  (rolling update + CreateSessionAPIToken) and the cookie Max-Age.
- RequireAuth accepts the goloom_session cookie when no bearer header; cookie-
  authenticated unsafe methods require a double-submit CSRF token
  (goloom_csrf cookie + X-CSRF-Token header). Bearer requests bypass CSRF.
- OIDC callback sets the session + CSRF cookies and 303-redirects (no token in
  the URL hash). New POST /v1/auth/session/token (token/recovery login → cookie)
  and POST /v1/auth/logout (clear cookies + revoke the session row).
- SPA: api client sends credentials + the CSRF header; "logged in" is decided by
  a /v1/me probe, not a stored token; recovery login uses session/token; sign-out
  calls logout. The dev "Browser session" panel keeps its bearer override.

Tests: cookie auth + CSRF (bearer bypass) in RequireAuth; session/token + logout
lifecycle; Playwright cookie-session spec (HttpOnly cookie, no localStorage
token, CSRF-less POST rejected); existing login e2e stays green.

Ops note: requires HTTPS in production (Secure cookie); SESSION_TTL is
configurable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
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
kreativmonkey/goloom!92
No description provided.