Skip to content

feat: enterprise-grade auth -- HttpOnly cookie sessions, CSRF protection, concurrent session control #1068

@Aureliolo

Description

@Aureliolo

Context

JWT tokens are currently stored in sessionStorage (PR #1067 migrated from localStorage). While tab-scoped storage reduces XSS blast radius, the token remains accessible to any JavaScript running in the page context. For enterprise multi-user deployments, the auth layer needs to be fully hardened.

Previous tracking: #924 deferred HttpOnly cookies as "separate concern -- full auth rework". #1060 item 1 implemented sessionStorage as the interim step. This issue covers the full migration.

Scope

1. HttpOnly cookie-based sessions (backend)

Replace the current flow (JWT returned in response body, stored in JS, sent via Authorization: Bearer header) with server-managed HttpOnly cookies.

Login/setup response:

  • Set Set-Cookie: session=<JWT>; HttpOnly; Secure; SameSite=Strict; Path=/api; Max-Age=<expiry>
  • Response body returns only { success: true, expires_in: N } (no token)
  • Secure flag ensures cookie is only sent over HTTPS (allow override for local dev via config)

Auth middleware:

  • Read JWT from cookie (session) instead of Authorization header
  • Support both cookie and Authorization header during migration (configurable grace period, then header-only is rejected)
  • API key auth (X-API-Key header) remains unchanged

Logout:

  • POST /api/v1/auth/logout clears the cookie (Set-Cookie: session=; Max-Age=0; ...)
  • Revokes session in SessionStore (already implemented)

Password change:

  • Rotate cookie (issue new JWT, set new cookie, revoke old session)

2. CSRF protection (backend + frontend)

HttpOnly cookies are sent automatically by the browser, making the app vulnerable to cross-site request forgery. CSRF protection is mandatory.

Backend (Litestar CSRF middleware):

  • Enable Litestar's built-in CSRF protection (CSRFConfig)
  • Generate CSRF token on session creation, deliver via a non-HttpOnly cookie (csrf_token) or response header (X-CSRF-Token)
  • Validate CSRF token on all state-mutating requests (POST, PUT, PATCH, DELETE)
  • Exempt: GET, HEAD, OPTIONS, health check, login, setup endpoints
  • Exempt: API key-authenticated requests (no cookie = no CSRF risk)

Frontend:

  • Read CSRF token from cookie or meta tag
  • Attach X-CSRF-Token header to all mutating API calls via Axios request interceptor
  • SSE (providers.ts pullModel) must also include the CSRF token

3. Concurrent session management

Enterprise multi-user deployments need session control beyond the current single-user model.

Per-user session limits:

  • Add max_concurrent_sessions to AuthConfig (default: 5, 0 = unlimited)
  • On login, if user exceeds limit, revoke oldest session before creating new one
  • Admin API: GET /api/v1/admin/sessions (list all active sessions), DELETE /api/v1/admin/sessions/{id} (revoke specific session)

Session metadata:

  • Already tracked: ip_address, user_agent, created_at, last_active_at, expires_at
  • Add: device_name (parsed from User-Agent), location (optional, from IP if configured)

Dashboard session management:

  • New section in Settings page: "Active Sessions"
  • Show: device, IP, last active, current session indicator
  • Action: "Revoke" button per session, "Revoke all other sessions" bulk action
  • Real-time updates via WebSocket when a session is revoked

4. Token rotation and refresh

  • Add optional refresh token flow (refresh_token cookie, longer-lived, used to obtain new session cookie)
  • POST /api/v1/auth/refresh endpoint
  • Configurable: jwt_refresh_enabled (default: false), jwt_refresh_expiry_days (default: 7)
  • Refresh token rotation: each refresh issues a new refresh token and invalidates the old one (prevents replay)

5. Security headers and hardening

  • Add Clear-Site-Data: "cookies" header on logout (instructs browser to purge all cookies for the origin)
  • Ensure SameSite=Strict prevents cookie from being sent in cross-origin requests
  • Add rate limiting on login endpoint (already tracked separately, but should be coordinated)
  • Add account lockout after N failed login attempts (configurable, default: 10 in 15 minutes)

6. Frontend migration

  • Remove all sessionStorage auth token operations from auth.ts, client.ts, providers.ts
  • Remove Authorization: Bearer header from Axios request interceptor (cookie is sent automatically)
  • Add CSRF token interceptor
  • Update clearAuth() to call logout endpoint (which clears the cookie server-side)
  • Update 401 handler to redirect to login (cookie already cleared by server)
  • Remove expiry timer logic (server controls cookie expiry)
  • Update SSE in providers.ts to use credentials: 'include' instead of manual token header
  • Add session management UI to Settings page

Acceptance criteria

  • Login/setup sets HttpOnly Secure SameSite=Strict cookie instead of returning JWT in body
  • Auth middleware reads JWT from cookie (with backward-compat header support during migration)
  • CSRF protection enabled for all state-mutating requests
  • Frontend sends CSRF token via header, no longer touches JWT directly
  • sessionStorage auth code fully removed from frontend
  • Concurrent session limit enforced (default 5)
  • Session management UI in Settings (list, revoke, revoke-all)
  • Logout clears cookie server-side + client-side
  • Password change rotates session cookie
  • All existing auth tests updated
  • New tests: CSRF validation, cookie handling, session limits, refresh flow
  • Security doc (docs/security.md) updated with new threat model

Design spec reference

  • API page (auth section)
  • Operations page (security headers)

Migration strategy

  1. Backend: add cookie-based auth alongside header-based (both accepted)
  2. Frontend: switch to cookie-based
  3. Backend: deprecate header-based JWT auth (log warning)
  4. Backend: remove header-based JWT support (breaking change, major version bump)

Non-goals

  • OAuth2 / OIDC integration (separate issue)
  • Multi-factor authentication (separate issue)
  • SSO / SAML (separate issue)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions