fix(dashboard): _require_token endpoints all 401 behind the OAuth gate#42578
Conversation
In gated/OAuth mode (non-loopback bind without --insecure) the dashboard
authenticates the SPA via a session cookie and deliberately does NOT inject
the legacy ephemeral _SESSION_TOKEN into index.html. gated_auth_middleware
verifies the cookie and attaches request.state.session before any non-public
/api/ route runs; the legacy auth_middleware short-circuits in this mode too.
But several handlers call _require_token() directly, which only validated the
(absent) _SESSION_TOKEN header. So every cookie-authenticated request to those
endpoints 401'd — making plugin install/enable/disable, /api/dashboard/plugins/hub,
and the other _require_token routes permanently unreachable behind the gate.
In the UI this surfaced as a 401: {"detail":"Unauthorized"} popup on plugin
install for any publicly-bound (e.g. Fly-hosted NAS) dashboard.
Fix: _require_token now defers to the active gate. When auth_required is True it
accepts the request iff the gate attached a verified session (and 401s otherwise);
loopback/--insecure behavior is unchanged (still validates the session token).
Adds two regression tests driving the full in-process stub OAuth round trip:
the install endpoint must NOT 401 a logged-in request, and must still 401 with
no cookie. Verified the accept-test fails on the pre-fix code.
🔎 Lint report:
|
… gate The install popup was one symptom of a class-wide bug: all 14 endpoints that call _require_token directly (API-key reveal, provider validation, the OAuth-provider connect/disconnect flow, and plugin enable/disable/update/ delete/visibility/providers) 401'd cookie-authenticated requests in gated mode. Add a parametrized test hitting a representative spread (plugins/hub, env/reveal, providers/validate, an oauth provider route, agent-plugin enable) asserting a logged-in caller is never 401'd — proving the fix covers the class, not just agent-plugins/install.
|
✅ Verified — OAuth gate fix for Reviewed the diff for
The fix is correct and well-scoped — it only changes the auth decision point without modifying any downstream handler logic. No issues found. |
tonydwb
left a comment
There was a problem hiding this comment.
Code Review Summary
Verdict: Approved
Fixes a regression where _require_token endpoints 401'd cookie-authenticated requests behind the OAuth gate. The issue: _require_token checked for the ephemeral _SESSION_TOKEN which is NOT present in gated mode (the SPA uses session cookies there). The fix makes _require_token defer to the gate when auth_required is True, since the gate has already verified the cookie and attached request.state.session.
- Comprehensive test coverage: 3 new test functions covering the install endpoint + 5 other representative
_require_tokenroutes - Fix is correct and well-documented in the docstring
- No security concerns
Reviewed by Hermes Agent (cron batch)
NousResearch#42578) * fix(dashboard): let _require_token endpoints work behind the OAuth gate In gated/OAuth mode (non-loopback bind without --insecure) the dashboard authenticates the SPA via a session cookie and deliberately does NOT inject the legacy ephemeral _SESSION_TOKEN into index.html. gated_auth_middleware verifies the cookie and attaches request.state.session before any non-public /api/ route runs; the legacy auth_middleware short-circuits in this mode too. But several handlers call _require_token() directly, which only validated the (absent) _SESSION_TOKEN header. So every cookie-authenticated request to those endpoints 401'd — making plugin install/enable/disable, /api/dashboard/plugins/hub, and the other _require_token routes permanently unreachable behind the gate. In the UI this surfaced as a 401: {"detail":"Unauthorized"} popup on plugin install for any publicly-bound (e.g. Fly-hosted NAS) dashboard. Fix: _require_token now defers to the active gate. When auth_required is True it accepts the request iff the gate attached a verified session (and 401s otherwise); loopback/--insecure behavior is unchanged (still validates the session token). Adds two regression tests driving the full in-process stub OAuth round trip: the install endpoint must NOT 401 a logged-in request, and must still 401 with no cookie. Verified the accept-test fails on the pre-fix code. * test(dashboard): cover the whole _require_token route class under the gate The install popup was one symptom of a class-wide bug: all 14 endpoints that call _require_token directly (API-key reveal, provider validation, the OAuth-provider connect/disconnect flow, and plugin enable/disable/update/ delete/visibility/providers) 401'd cookie-authenticated requests in gated mode. Add a parametrized test hitting a representative spread (plugins/hub, env/reveal, providers/validate, an oauth provider route, agent-plugin enable) asserting a logged-in caller is never 401'd — proving the fix covers the class, not just agent-plugins/install.
#42578) * fix(dashboard): let _require_token endpoints work behind the OAuth gate In gated/OAuth mode (non-loopback bind without --insecure) the dashboard authenticates the SPA via a session cookie and deliberately does NOT inject the legacy ephemeral _SESSION_TOKEN into index.html. gated_auth_middleware verifies the cookie and attaches request.state.session before any non-public /api/ route runs; the legacy auth_middleware short-circuits in this mode too. But several handlers call _require_token() directly, which only validated the (absent) _SESSION_TOKEN header. So every cookie-authenticated request to those endpoints 401'd — making plugin install/enable/disable, /api/dashboard/plugins/hub, and the other _require_token routes permanently unreachable behind the gate. In the UI this surfaced as a 401: {"detail":"Unauthorized"} popup on plugin install for any publicly-bound (e.g. Fly-hosted NAS) dashboard. Fix: _require_token now defers to the active gate. When auth_required is True it accepts the request iff the gate attached a verified session (and 401s otherwise); loopback/--insecure behavior is unchanged (still validates the session token). Adds two regression tests driving the full in-process stub OAuth round trip: the install endpoint must NOT 401 a logged-in request, and must still 401 with no cookie. Verified the accept-test fails on the pre-fix code. * test(dashboard): cover the whole _require_token route class under the gate The install popup was one symptom of a class-wide bug: all 14 endpoints that call _require_token directly (API-key reveal, provider validation, the OAuth-provider connect/disconnect flow, and plugin enable/disable/update/ delete/visibility/providers) 401'd cookie-authenticated requests in gated mode. Add a parametrized test hitting a representative spread (plugins/hub, env/reveal, providers/validate, an oauth provider route, agent-plugin enable) asserting a logged-in caller is never 401'd — proving the fix covers the class, not just agent-plugins/install.
NousResearch#42578) * fix(dashboard): let _require_token endpoints work behind the OAuth gate In gated/OAuth mode (non-loopback bind without --insecure) the dashboard authenticates the SPA via a session cookie and deliberately does NOT inject the legacy ephemeral _SESSION_TOKEN into index.html. gated_auth_middleware verifies the cookie and attaches request.state.session before any non-public /api/ route runs; the legacy auth_middleware short-circuits in this mode too. But several handlers call _require_token() directly, which only validated the (absent) _SESSION_TOKEN header. So every cookie-authenticated request to those endpoints 401'd — making plugin install/enable/disable, /api/dashboard/plugins/hub, and the other _require_token routes permanently unreachable behind the gate. In the UI this surfaced as a 401: {"detail":"Unauthorized"} popup on plugin install for any publicly-bound (e.g. Fly-hosted NAS) dashboard. Fix: _require_token now defers to the active gate. When auth_required is True it accepts the request iff the gate attached a verified session (and 401s otherwise); loopback/--insecure behavior is unchanged (still validates the session token). Adds two regression tests driving the full in-process stub OAuth round trip: the install endpoint must NOT 401 a logged-in request, and must still 401 with no cookie. Verified the accept-test fails on the pre-fix code. * test(dashboard): cover the whole _require_token route class under the gate The install popup was one symptom of a class-wide bug: all 14 endpoints that call _require_token directly (API-key reveal, provider validation, the OAuth-provider connect/disconnect flow, and plugin enable/disable/update/ delete/visibility/providers) 401'd cookie-authenticated requests in gated mode. Add a parametrized test hitting a representative spread (plugins/hub, env/reveal, providers/validate, an oauth provider route, agent-plugin enable) asserting a logged-in caller is never 401'd — proving the fix covers the class, not just agent-plugins/install.
NousResearch#42578) * fix(dashboard): let _require_token endpoints work behind the OAuth gate In gated/OAuth mode (non-loopback bind without --insecure) the dashboard authenticates the SPA via a session cookie and deliberately does NOT inject the legacy ephemeral _SESSION_TOKEN into index.html. gated_auth_middleware verifies the cookie and attaches request.state.session before any non-public /api/ route runs; the legacy auth_middleware short-circuits in this mode too. But several handlers call _require_token() directly, which only validated the (absent) _SESSION_TOKEN header. So every cookie-authenticated request to those endpoints 401'd — making plugin install/enable/disable, /api/dashboard/plugins/hub, and the other _require_token routes permanently unreachable behind the gate. In the UI this surfaced as a 401: {"detail":"Unauthorized"} popup on plugin install for any publicly-bound (e.g. Fly-hosted NAS) dashboard. Fix: _require_token now defers to the active gate. When auth_required is True it accepts the request iff the gate attached a verified session (and 401s otherwise); loopback/--insecure behavior is unchanged (still validates the session token). Adds two regression tests driving the full in-process stub OAuth round trip: the install endpoint must NOT 401 a logged-in request, and must still 401 with no cookie. Verified the accept-test fails on the pre-fix code. * test(dashboard): cover the whole _require_token route class under the gate The install popup was one symptom of a class-wide bug: all 14 endpoints that call _require_token directly (API-key reveal, provider validation, the OAuth-provider connect/disconnect flow, and plugin enable/disable/update/ delete/visibility/providers) 401'd cookie-authenticated requests in gated mode. Add a parametrized test hitting a representative spread (plugins/hub, env/reveal, providers/validate, an oauth provider route, agent-plugin enable) asserting a logged-in caller is never 401'd — proving the fix covers the class, not just agent-plugins/install.
Impact
On any publicly-bound dashboard (the OAuth-gated mode — Fly-hosted NAS agents, or any non-loopback bind without
--insecure), every endpoint that calls_require_tokendirectly always 401s with a popup:The reported symptom was plugin install failing, but the same bug breaks the whole class of
_require_tokenendpoints behind the gate (14 in total):POST /api/env/reveal)POST /api/providers/validate)/api/providers/oauth/{id}/start,/submit,DELETE …/{id},DELETE …/sessions/{sid})/api/dashboard/plugins/hub,agent-plugins/install,…/enable,…/disable,…/update,DELETE …/{name},plugin-providers,plugins/{name}/visibility)This is the common path for hosted dashboards, not an exotic config. Loopback (local
hermes dashboard) users are not affected by this bug — their token-injection path is unchanged.Root cause
Two auth schemes protect the dashboard, exactly one active per bind:
--insecure(auth_requiredFalse): the ephemeral_SESSION_TOKENis injected into the SPA HTML and echoed back viaX-Hermes-Session-Token.auth_middlewarevalidates it.auth_requiredTrue):_SESSION_TOKENis not injected (_serve_index, web_server.py:9003-9018); the SPA authenticates with a session cookie.gated_auth_middlewareverifies the cookie, attachesrequest.state.session, and 401s anything unauthenticated before the handler runs.auth_middlewarecorrectly short-circuits in this mode (web_server.py:376-377).The bug: the 14 handlers above call
_require_token(request)directly, which only checked the legacy_SESSION_TOKENheader. In gated mode there is no such header, so the check 401'd every cookie-authenticated request — even though the gate had already authenticated it.Fix
_require_tokennow defers to the active gate. Whenauth_requiredis True it accepts the request iff the gate attached a verifiedrequest.state.session(and 401s otherwise — so the endpoints stay protected, they don't become public). Loopback /--insecurebehavior is byte-for-byte unchanged: it still validates the session token. Because the fix lives inside_require_token, all 14 call sites are covered.Verified none of the 14 routes are in
PUBLIC_API_PATHS(an allowlisted route would never get a gate-attached session and would then 401 after the fix). The gate matchesPUBLIC_API_PATHSby exact string, so/api/dashboard/pluginsbeing public does not leak to/api/dashboard/plugins/hubor…/{name}/visibility.One-function change in
hermes_cli/web_server.py.Tests
In
tests/hermes_cli/test_dashboard_auth_middleware.py, all driving the full in-process stub OAuth round trip (real gate, real cookies, real handler — nothing mocked):test_gated_require_token_endpoint_accepts_cookie_session— logged-in POST to install must not 401 (reaches the handler's own 400).test_gated_require_token_endpoint_still_rejects_no_cookie— unauthenticated POST must still 401.test_gated_require_token_routes_accept_cookie_session— parametrized over a representative spread (plugins/hub,env/reveal,providers/validate, an oauth provider route,agent-plugins/{name}/enable) proving the fix covers the whole class.Verified the accept-test fails on the pre-fix code with the exact
401: {"detail":"Unauthorized"}bug, and passes with the fix. Full dashboard-auth + web_server suites green (346 passed). Confirmed the loopback_require_tokenmatrix is unchanged (valid token → OK, missing/bad → 401).