hermes-agent/hermes_cli/dashboard_auth
Ben 034ad95fed fix(dashboard-auth): propagate next= through login page + PKCE cookie
The gate's _unauth_response set next=<path> on the /login redirect URL,
but nothing downstream read it: render_login_html ignored next=,
auth_login dropped it, and auth_callback read next= from its own query
string — which an IDP never sets on the callback URL (real IDPs only
echo back code+state). The _validate_post_login_target plumbing in the
callback was unreachable on the happy path, so users always landed on
"/" regardless of what they originally requested.

Worse: reading next= from the callback URL was a latent open-redirect
sink, since an attacker could craft /auth/callback?...&next=/admin and
have the server honour it post-auth.

Fix carries next= through the round trip on a server-controlled channel:

  1. login_page reads request.query_params['next'] and passes it (post-
     validation) to render_login_html.
  2. render_login_html threads next= URL-encoded into each provider
     button's href, with HTML-attribute escaping as defence in depth.
  3. auth_login accepts ?next= as a query param, re-validates, and
     appends it as a fourth segment (next=<urlquoted>) in the PKCE
     cookie payload alongside provider/state/verifier.
  4. auth_callback no longer accepts a next: str = "" query param. It
     parses next= out of the PKCE cookie and validates that with the
     same same-origin rules. Any attacker-supplied ?next= on the
     callback URL is silently ignored — server-only carrier.

Test coverage adds three classes:

  - TestAuthCallbackNext drives /login → /auth/login → IDP-bounce →
    /auth/callback end-to-end without smuggling next= onto the callback
    URL (which is what the previous tests did and why they didn't
    catch the bug). Includes test_attacker_callback_next_param_is_ignored
    to pin the security property that the URL value is never read.
  - TestRenderLoginHtmlNext covers the rendering function at the
    unit boundary so a regression that drops next_path is caught
    without spinning up the full app.
  - TestAuthLoginPkceCookieNext inspects the Set-Cookie header on
    /auth/login responses so a regression in cookie encoding is caught
    without driving the full round trip.

Mutation-tested: reverting auth_callback to read next= from the URL
trips 3 of 6 TestAuthCallbackNext tests (the safe-path and attacker-
hardening ones), confirming the suite discriminates between the cookie
read and the URL read.
2026-05-27 02:12:27 -07:00
..
__init__.py feat(dashboard-auth): define DashboardAuthProvider ABC + Session dataclass 2026-05-27 02:12:27 -07:00
audit.py feat(dashboard-auth): single-use WS tickets + POST /api/auth/ws-ticket 2026-05-27 02:12:27 -07:00
base.py feat(dashboard-auth): define DashboardAuthProvider ABC + Session dataclass 2026-05-27 02:12:27 -07:00
cookies.py feat(dashboard-auth): Phase 6 — 401 re-auth envelope + next= propagation 2026-05-27 02:12:27 -07:00
login_page.py fix(dashboard-auth): propagate next= through login page + PKCE cookie 2026-05-27 02:12:27 -07:00
middleware.py feat(dashboard-auth): Phase 6 — 401 re-auth envelope + next= propagation 2026-05-27 02:12:27 -07:00
registry.py feat(dashboard-auth): define DashboardAuthProvider ABC + Session dataclass 2026-05-27 02:12:27 -07:00
routes.py fix(dashboard-auth): propagate next= through login page + PKCE cookie 2026-05-27 02:12:27 -07:00
ws_tickets.py feat(dashboard-auth): single-use WS tickets + POST /api/auth/ws-ticket 2026-05-27 02:12:27 -07:00