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
Design spec reference
- API page (auth section)
- Operations page (security headers)
Migration strategy
- Backend: add cookie-based auth alongside header-based (both accepted)
- Frontend: switch to cookie-based
- Backend: deprecate header-based JWT auth (log warning)
- 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)
Context
JWT tokens are currently stored in
sessionStorage(PR #1067 migrated fromlocalStorage). 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
sessionStorageas 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: Bearerheader) with server-managed HttpOnly cookies.Login/setup response:
Set-Cookie: session=<JWT>; HttpOnly; Secure; SameSite=Strict; Path=/api; Max-Age=<expiry>{ success: true, expires_in: N }(no token)Secureflag ensures cookie is only sent over HTTPS (allow override for local dev via config)Auth middleware:
session) instead ofAuthorizationheaderAuthorizationheader during migration (configurable grace period, then header-only is rejected)X-API-Keyheader) remains unchangedLogout:
POST /api/v1/auth/logoutclears the cookie (Set-Cookie: session=; Max-Age=0; ...)SessionStore(already implemented)Password change:
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):
CSRFConfig)csrf_token) or response header (X-CSRF-Token)POST,PUT,PATCH,DELETE)GET,HEAD,OPTIONS, health check, login, setup endpointsFrontend:
X-CSRF-Tokenheader to all mutating API calls via Axios request interceptorproviders.ts pullModel) must also include the CSRF token3. Concurrent session management
Enterprise multi-user deployments need session control beyond the current single-user model.
Per-user session limits:
max_concurrent_sessionstoAuthConfig(default: 5, 0 = unlimited)GET /api/v1/admin/sessions(list all active sessions),DELETE /api/v1/admin/sessions/{id}(revoke specific session)Session metadata:
ip_address,user_agent,created_at,last_active_at,expires_atdevice_name(parsed from User-Agent),location(optional, from IP if configured)Dashboard session management:
4. Token rotation and refresh
refresh_tokencookie, longer-lived, used to obtain new session cookie)POST /api/v1/auth/refreshendpointjwt_refresh_enabled(default: false),jwt_refresh_expiry_days(default: 7)5. Security headers and hardening
Clear-Site-Data: "cookies"header on logout (instructs browser to purge all cookies for the origin)SameSite=Strictprevents cookie from being sent in cross-origin requests6. Frontend migration
sessionStorageauth token operations fromauth.ts,client.ts,providers.tsAuthorization: Bearerheader from Axios request interceptor (cookie is sent automatically)clearAuth()to call logout endpoint (which clears the cookie server-side)providers.tsto usecredentials: 'include'instead of manual token headerAcceptance criteria
sessionStorageauth code fully removed from frontenddocs/security.md) updated with new threat modelDesign spec reference
Migration strategy
Non-goals