mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-04 07:31:58 +00:00
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.
This commit is contained in:
parent
12142c0ca1
commit
884f0da82a
5 changed files with 838 additions and 0 deletions
|
|
@ -249,9 +249,29 @@ async def host_header_middleware(request: Request, call_next):
|
|||
return await call_next(request)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dashboard OAuth auth gate — engaged only when start_server flags the
|
||||
# bind as non-loopback-without-insecure. No-op pass-through in loopback
|
||||
# mode so the legacy auth_middleware (below) handles those binds via
|
||||
# the injected ``_SESSION_TOKEN``. Registered between host_header and
|
||||
# auth_middleware so the order is: host check → cookie auth → token auth.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def _dashboard_auth_gate(request: Request, call_next):
|
||||
from hermes_cli.dashboard_auth.middleware import gated_auth_middleware
|
||||
return await gated_auth_middleware(request, call_next)
|
||||
|
||||
|
||||
@app.middleware("http")
|
||||
async def auth_middleware(request: Request, call_next):
|
||||
"""Require the session token on all /api/ routes except the public list."""
|
||||
# When the OAuth gate is active, cookie-based auth (gated_auth_middleware
|
||||
# above) is authoritative. The legacy _SESSION_TOKEN path is loopback-only
|
||||
# and is skipped here so the gate's session attachment isn't overridden.
|
||||
if getattr(request.app.state, "auth_required", False):
|
||||
return await call_next(request)
|
||||
path = request.url.path
|
||||
if path.startswith("/api/") and path not in _PUBLIC_API_PATHS:
|
||||
if not _has_valid_session_token(request):
|
||||
|
|
@ -4524,6 +4544,13 @@ def _mount_plugin_api_routes():
|
|||
# Mount plugin API routes before the SPA catch-all.
|
||||
_mount_plugin_api_routes()
|
||||
|
||||
# Mount the dashboard auth routes (/login, /auth/*, /api/auth/*) before the
|
||||
# SPA catch-all so /{full_path:path} doesn't swallow them. These are
|
||||
# always mounted — the gate middleware decides whether to enforce auth,
|
||||
# not whether the routes exist.
|
||||
from hermes_cli.dashboard_auth.routes import router as _dashboard_auth_router # noqa: E402
|
||||
app.include_router(_dashboard_auth_router)
|
||||
|
||||
mount_spa(app)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue