Skip to content

feat(dashboard): auto-initiate portal SSO redirect on unauthenticated load - #54846

Merged
teknium1 merged 2 commits into
mainfrom
hermes/hermes-1c5172e4
Jun 29, 2026
Merged

feat(dashboard): auto-initiate portal SSO redirect on unauthenticated load#54846
teknium1 merged 2 commits into
mainfrom
hermes/hermes-1c5172e4

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

Clicking a hosted-agent dashboard link while signed in to the Nous portal now lands you in the dashboard with zero clicks — the unauthenticated /login interstitial is replaced by an automatic silent OAuth bounce.

The portal /oauth/authorize already auto-approves any current org member and 302s straight back when the user holds a portal session, so the old click-through interstitial was pure friction for the common case.

Changes

  • middleware.py: new _auto_sso_response() — on an unauthenticated HTML document load with the no_cookie branch, 302 to the existing /auth/login OAuth-initiation route instead of rendering /login. Fires only when it's a document load (not /api/*), exactly one interactive provider is registered, and the one-shot loop-guard marker is absent.
  • cookies.py: short-lived (60s) hermes_sso_attempt loop-guard cookie — set on the bounce, read on return, cleared on the /login fallback and on successful callback. A boolean breadcrumb, no secret.
  • routes.py: clear the guard marker on a successful /auth/callback.
  • Carries X-Forwarded-Prefix and the same-origin next= exactly as the old /login redirect did.

Security

Removes a human click, not a security check. The redirect lands on the unchanged PKCE auth-code flow — token verification, aud/redirect-URI checks, and org-membership checks are all untouched. /api/* fetches still get the 401 JSON envelope (never a 302 a fetch() would opaquely follow into the cross-origin OAuth dance). With two-or-more providers the /login chooser still renders. Provider name is quote()-escaped; next= runs through the existing same-origin open-redirect filter.

Validation

Before After
Unauth HTML load, 1 provider /login interstitial (click) silent 302 → /auth/login
Return still unauthenticated (n/a) guard trips → /login once, marker cleared
/api/* unauth fetch 401 JSON 401 JSON (unchanged)
2+ providers /login chooser /login chooser (unchanged)

99/99 dashboard-auth tests pass (test_dashboard_auth_401_reauth, _middleware, _prefix). New TestAutoSsoRedirect covers the bounce, the one-shot guard, the /api/* carve-out, and the multi-provider chooser.

Salvage note

@benbarclay authored the substantive change (commit preserved via rebase). Follow-up on top: dropped the redundant _interactive_providers() helper — list_session_providers() already filters on supports_session=True, so the call site now uses it directly.

Infographic

Auto-SSO redirect infographic

benbarclay and others added 2 commits June 29, 2026 03:56
… load

When the dashboard gateway has no local session cookie, it rendered a
click-through /login interstitial — even though the Nous portal's
/oauth/authorize auto-approves any current member of the dashboard's org
and is a silent 302 when the user already holds a portal session. For the
common case (clicking a hosted-agent dashboard link while signed in to the
portal) that interstitial click is pure friction.

This makes the gate auto-initiate the OAuth redirect on an unauthenticated
HTML document load instead of rendering the interstitial, when exactly one
interactive provider is registered. A one-shot loop-guard cookie
(hermes_sso_attempt, 60s TTL) ensures that a genuinely absent portal
session (the portal bounces back still-unauthenticated) falls back to the
/login page after exactly one bounce rather than ping-ponging forever. The
marker is cleared on a successful callback and whenever the gate falls back
to /login.

Security: this removes a human CLICK, not a security check. The redirect
lands on the existing /auth/login route and runs the unchanged PKCE
auth-code flow; token verification, audience checks, redirect-URI match,
and org-membership checks are all untouched. /api/* fetches still get the
401 JSON envelope (never a 302 a fetch() would follow opaquely), and with
two or more providers the /login chooser still renders.

Phase 1 of the cloud-auto-discovery work.
list_session_providers() already filters on supports_session=True, so the
new helper re-filtered an already-filtered list. Call it directly at the
single auto-SSO call site.
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Lint report: hermes/hermes-1c5172e4 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: 11716 on HEAD, 11716 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 6160 pre-existing issues carried over.

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

@alt-glitch alt-glitch added type/feature New feature or request comp/dashboard Web dashboard / control panel UI (dashboard/, landing) area/auth Authentication, OAuth, credential pools P3 Low — cosmetic, nice to have labels Jun 29, 2026
@teknium1
teknium1 merged commit 61f56d2 into main Jun 29, 2026
30 checks passed
@teknium1
teknium1 deleted the hermes/hermes-1c5172e4 branch June 29, 2026 11:25
This was referenced Jul 3, 2026
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/dashboard Web dashboard / control panel UI (dashboard/, landing) P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants