Commit Graph
2 Commits
Author SHA1 Message Date
Ben b69fce9c86 feat(dashboard-auth): single-use WS tickets + POST /api/auth/ws-ticket
Phase 5 task 5.1. Browsers cannot set Authorization on a WebSocket
upgrade, so in gated mode the SPA needs an alternative way to bind the
upgrade to its authenticated session.

  hermes_cli/dashboard_auth/ws_tickets.py — in-memory single-use ticket
  store with 30s TTL. Thread-safe (threading.Lock), token_urlsafe(32)
  values, ticket value truncated to 8 chars in error messages for log
  hygiene. Module-level state with _reset_for_tests() helper.

  hermes_cli/dashboard_auth/routes.py — adds POST /api/auth/ws-ticket.
  Auth-required (the gate middleware already attaches Session to
  request.state.session). Returns {ticket, ttl_seconds}; emits
  WS_TICKET_MINTED audit event with user_id + provider + ip.

  hermes_cli/dashboard_auth/audit.py — adds WS_TICKET_REJECTED enum
  value for the consume-side rejection event (wired into the WS
  endpoints in task 5.2).

11 new tests covering round-trip, single-use, TTL boundary, unknown
ticket rejection, secret-hygiene truncation in error messages, and
concurrent mint+consume from 20 threads.
2026-05-27 02:12:27 -07:00
Ben 5b17eab67a feat(dashboard-auth): auth gate middleware + /auth/* routes + /login HTML
Phase 3, Tasks 3.2 + 3.3 + 3.4. These three pieces are mutually
dependent so they land together.

middleware.py - gated_auth_middleware engages when app.state.auth_required
is True.  Allowlists /login, /auth/*, /api/auth/providers, and static
asset paths; everything else demands a valid session_at cookie.  Verifies
by trying every registered provider's verify_session in turn (multi-
provider stack); attaches verified Session to request.state.session.
Returns 401 JSON for /api/* and 302 -> /login for HTML.  ProviderError
during verify -> 503.

routes.py - APIRouter with:
  GET  /login              server-rendered HTML
  GET  /auth/login?provider=N  302 to IDP + PKCE cookie
  GET  /auth/callback?code,state  completes login, sets session cookies
  POST /auth/logout        clears cookies + best-effort revoke
  GET  /api/auth/providers public bootstrap endpoint (503 if zero)
  GET  /api/auth/me        verified session as JSON (auth-required)

login_page.py - Inline-CSS HTML template, no React, no JavaScript.

web_server.py - Mounted gated_auth_middleware between host_header and
auth_middleware (FastAPI runs middlewares in registration order: host
check -> cookie auth -> token auth).  auth_middleware short-circuits
when auth_required so cookie auth is authoritative in gated mode.
Router is included before mount_spa so the catch-all doesn't swallow
/login or /auth/*.

17 new behavioural tests; loopback regression harness still green.
2026-05-27 02:12:27 -07:00