Skip to content

fix(dashboard): _require_token endpoints all 401 behind the OAuth gate#42578

Merged
kshitijk4poor merged 2 commits into
mainfrom
fix/require-token-gated-cookie
Jun 10, 2026
Merged

fix(dashboard): _require_token endpoints all 401 behind the OAuth gate#42578
kshitijk4poor merged 2 commits into
mainfrom
fix/require-token-gated-cookie

Conversation

@benbarclay

@benbarclay benbarclay commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

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_token directly always 401s with a popup:

401: {"detail":"Unauthorized"}

The reported symptom was plugin install failing, but the same bug breaks the whole class of _require_token endpoints behind the gate (14 in total):

  • API-key reveal (POST /api/env/reveal)
  • Provider key validation (POST /api/providers/validate)
  • The entire OAuth-provider connect/disconnect flow (/api/providers/oauth/{id}/start, /submit, DELETE …/{id}, DELETE …/sessions/{sid})
  • Plugin management (/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:

  • Loopback / --insecure (auth_required False): the ephemeral _SESSION_TOKEN is injected into the SPA HTML and echoed back via X-Hermes-Session-Token. auth_middleware validates it.
  • Gated / OAuth (auth_required True): _SESSION_TOKEN is not injected (_serve_index, web_server.py:9003-9018); the SPA authenticates with a session cookie. gated_auth_middleware verifies the cookie, attaches request.state.session, and 401s anything unauthenticated before the handler runs. auth_middleware correctly 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_TOKEN header. 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_token now defers to the active gate. When auth_required is True it accepts the request iff the gate attached a verified request.state.session (and 401s otherwise — so the endpoints stay protected, they don't become public). Loopback / --insecure behavior 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 matches PUBLIC_API_PATHS by exact string, so /api/dashboard/plugins being public does not leak to /api/dashboard/plugins/hub or …/{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_token matrix is unchanged (valid token → OK, missing/bad → 401).

Note: a separate follow-up PR will address the loopback stale-token UX (token rotates on every dashboard restart; a tab held across a restart can surface the same popup). Kept out of this PR to stay lane-pure.

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.
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: fix/require-token-gated-cookie vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 10579 on HEAD, 10579 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 5557 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

… 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.
@benbarclay benbarclay changed the title fix(dashboard): let _require_token endpoints work behind the OAuth gate fix(dashboard): _require_token endpoints all 401 behind the OAuth gate Jun 9, 2026
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/cli CLI entry point, hermes_cli/, setup wizard area/auth Authentication, OAuth, credential pools labels Jun 9, 2026
@liuhao1024

Copy link
Copy Markdown
Contributor

✅ Verified — OAuth gate fix for _require_token endpoints

Reviewed the diff for hermes_cli/web_server.py::_require_token and the regression tests.

  • Gate delegation: Confirmed that when auth_required=True, _require_token checks request.state.session (set by gated_auth_middleware) before the handler runs. No token is required — the gate is authoritative.
  • Backward compat: Loopback/--insecure mode still validates the ephemeral _SESSION_TOKEN as before. No behavior change for non-gated deployments.
  • Test coverage: 3 test functions covering (1) cookie-authenticated request passes, (2) unauthenticated request still 401s, (3) parametrized sweep of all 5 representative _require_token routes. The _complete_stub_login helper correctly walks the full OAuth round-trip.

The fix is correct and well-scoped — it only changes the auth decision point without modifying any downstream handler logic. No issues found.

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_token routes
  • Fix is correct and well-documented in the docstring
  • No security concerns

Reviewed by Hermes Agent (cron batch)

@kshitijk4poor kshitijk4poor merged commit 63a421d into main Jun 10, 2026
22 of 23 checks passed
@kshitijk4poor kshitijk4poor deleted the fix/require-token-gated-cookie branch June 10, 2026 05:57
changman pushed a commit to changman/hermes-agent that referenced this pull request Jun 10, 2026
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.
alt-glitch pushed a commit that referenced this pull request Jun 14, 2026
#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.
davidgut1982 pushed a commit to davidgut1982/hermes-agent that referenced this pull request Jun 17, 2026
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.
T02200059 pushed a commit to T02200059/hermes-agent that referenced this pull request Jun 18, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants