Skip to content

fix: dashboard auth crash with NotImplementedError for password-only providers - #56886

Closed
zxcasongs wants to merge 5 commits into
NousResearch:mainfrom
zxcasongs:fix/dashboard-auth-password-only-provider
Closed

fix: dashboard auth crash with NotImplementedError for password-only providers#56886
zxcasongs wants to merge 5 commits into
NousResearch:mainfrom
zxcasongs:fix/dashboard-auth-password-only-provider

Conversation

@zxcasongs

Copy link
Copy Markdown
Contributor

Fix: Dashboard auth crashes with NotImplementedError for password-only providers

Summary

When a dashboard is configured with only a BasicAuthProvider (username/password auth, no OAuth IDP), every unauthenticated request crashes with a 500 NotImplementedError instead of showing the login page. The user can never reach the dashboard.

Root cause: The auto-SSO middleware (_auto_sso_response) and the /auth/login route both assume the sole registered provider supports the OAuth redirect flow (start_login). BasicAuthProvider sets supports_password = True and intentionally raises NotImplementedError from start_login — it has no OAuth flow, only a credential form. The code never checks supports_password before calling start_login, so it blindly invokes a method that raises.

Trace:

  1. User visits dashboard with no session cookie
  2. gated_auth_middleware → no cookie → calls _auto_sso_response()
  3. _auto_sso_response sees exactly 1 provider (basic) → auto-redirects to /auth/login?provider=basic
  4. /auth/login route calls p.start_login(redirect_uri=...)
  5. BasicAuthProvider.start_login() → raises NotImplementedError
  6. Unhandled → 500 Internal Server Error

The fix

Two changes, both checking supports_password before attempting the OAuth path:

1. hermes_cli/dashboard_auth/middleware.py_auto_sso_response

When the sole provider is password-only, return None (fall through to the normal /login redirect). The login page already renders a credential form for supports_password providers.

     provider = providers[0]
+    if getattr(provider, "supports_password", False):
+        # Password-only providers have no OAuth redirect flow — they
+        # need the login page's credential form. Fall through to /login.
+        return None
     prefix = prefix_from_request(request)
     next_param = _safe_next_target(request)

2. hermes_cli/dashboard_auth/routes.pyauth_login

When someone hits /auth/login?provider=basic directly (e.g. bookmark, manual URL), redirect to /login instead of calling start_login and crashing.

     if not getattr(p, "supports_session", True):
         raise HTTPException(
             status_code=404,
             detail=f"Provider does not support interactive login: {provider!r}",
         )

+    if getattr(p, "supports_password", False):
+        # Password-only providers have no OAuth redirect flow.
+        # Redirect to the login page which renders the credential form.
+        from urllib.parse import quote
+        target = "/login"
+        if next:
+            target = f"{target}?next={quote(next, safe='')}"
+        return RedirectResponse(url=target, status_code=302)
+
     try:
         ls = p.start_login(redirect_uri=_redirect_uri(request))
     except ProviderError as e:

Why check supports_password (not provider name)?

Both checks use getattr(p, "supports_password", False) — the same flag the login page already uses to decide between rendering a credential form vs. an OAuth button. This keeps the fix generic: any future password-only provider benefits automatically, with no special-casing by name.

Impact

  • Before: Password-only dashboard is completely unusable — 500 on every page load.
  • After: Password-only dashboard works as designed — login page renders the username/password form, user POSTs to /auth/password-login (the existing, already-working path), gets session cookies.
  • OAuth providers: Completely unaffected. supports_password is False for OAuth providers, so both new checks are no-ops.
  • Mixed (OAuth + password): Unaffected. _auto_sso_response already returns None when len(providers) != 1.

Testing

Reproduce the original crash with a minimal BasicAuthProvider-only setup:

# Start dashboard with only basic auth configured
HERMES_DASHBOARD_BASIC_AUTH_USERNAME=admin \
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD=secret \
python -m hermes_cli dashboard --host 0.0.0.0 --port 9119 --no-open

Before fix: curl http://localhost:9119/ → 500 NotImplementedError: BasicAuthProvider is password-only

After fix: curl http://localhost:9119/ → 302 → /login (renders credential form)

# Login via the password endpoint (existing, already working)
curl -X POST http://localhost:9119/auth/password-login \
  -H 'Content-Type: application/json' \
  -d '{"provider":"basic","username":"admin","password":"secret"}'
# → {"ok": true, "next": "/"} + session cookies

Backwards compatibility

No config changes, no API changes, no migration. Existing OAuth-only and mixed deployments are completely unaffected — the new supports_password checks are no-ops for any provider where supports_password is False (the default).

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/dashboard Web dashboard / control panel UI (dashboard/, landing) area/auth Authentication, OAuth, credential pools labels Jul 2, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related to the dashboard password-only-provider NotImplementedError crash cluster: #55130 / #55498 (auto-SSO 500 issue), #55985 (logout crash-loop issue), and the competing fix PRs #54887 (middleware _auto_sso_response guard only), #55988 (route-level 400 guard), #55993 (route try/except redirect).

This PR is a superset: it guards both sites in one change — _auto_sso_response (middleware, like #54887) and auth_login (routes, proactive redirect to /login, covering the logout/direct /auth/login path that the middleware-only fixes miss). Flagging for a maintainer to pick between the narrow (#54887 / #55988 / #55993) and this combined approach.

…providers

When only a BasicAuthProvider (password-only, no OAuth IDP) is
registered, every unauthenticated request crashes with 500
NotImplementedError because:

1. _auto_sso_response auto-redirects to /auth/login?provider=basic
   (assumes the sole provider supports the OAuth redirect flow)
2. /auth/login route calls p.start_login() which raises NotImplementedError
   for password-only providers

Fix: check supports_password before attempting the OAuth path in both
locations. Password-only providers fall through to /login which renders
the credential form (the already-working POST /auth/password-login path).

OAuth and mixed providers are completely unaffected — the new checks
are no-ops when supports_password is False (the default).
@sbosshardt

Copy link
Copy Markdown

I encountered this exact bug with my deployment and (with an agent) validated a local fix end-to-end.

Environment / repro shape on our side:

  • reverse-proxied dashboard
  • dashboard.basic_auth enabled
  • dashboard.public_url set
  • exactly one configured auth provider, with supports_password=True

Observed behavior matched this PR:

  • first unauthenticated dashboard hit auto-redirected into /auth/login?provider=basic
  • BasicAuthProvider.start_login() raised NotImplementedError
  • result was HTTP 500 instead of the login form

We carried a local fix for this on our branch and it ended up very close to this PR, but with a few small deltas that may be worth folding in here:

  1. Validate next before reflecting it back

    • use _validate_post_login_target(next) on the /auth/login recovery path before redirecting back to /login?...
  2. Preserve request prefix on the fallback redirect

    • build the recovery redirect with _prefix(request) so /auth/login?provider=<password-provider> correctly falls back to the prefixed login page when Hermes is behind a forwarded path-prefix / reverse-proxy setup
  3. Add focused regression coverage for both broken entry points

    • first dashboard hit with a single password-only provider should land on /login?next=..., not auto-OAuth
    • direct /auth/login?provider=<password-provider>&next=... should recover to /login?next=..., not 500

In our local patch the route-level recovery uses 303 (instead of 302), though this is less important than the three points above. The meaningful differences are the safe-next handling, prefix-aware fallback, and the regression tests.

Happy to post the exact minimal test cases / follow-up patch if that would help move this forward.

@zxcasongs

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed feedback — all three points are valid and I'm folding them in:

  1. Validate next before reflecting it back — switching to _validate_post_login_target(next) on the /auth/login recovery path. The current quote(next, safe='') was an oversight; the OAuth callback path already validates, the password-only fallback should too.

  2. Preserve request prefix on the fallback redirect — using _prefix(request) so the redirect respects X-Forwarded-Prefix in reverse-proxied deployments. Hardcoded /login would break behind a path-prefix proxy.

  3. Regression tests — adding focused coverage for both entry points:

    • First dashboard hit with a single password-only provider → lands on /login?next=..., not auto-OAuth
    • Direct /auth/login?provider=<password-provider>&next=... → recovers to /login?next=..., not 500
    • Open-redirect next=https://evil.example and protocol-relative next=//evil.example are dropped
    • OAuth-only provider (StubAuthProvider) still works via /auth/login (no regression)

Also switching the route-level recovery to 303 (POST→GET semantics for the redirect, matching your suggestion).

Pushing the update shortly.

…irect, regression tests

- routes.py: use _validate_post_login_target(next) instead of raw quote(next)
  to prevent open-redirect via malicious next= values
- routes.py: use _prefix(request) for prefix-aware fallback redirect so
  reverse-proxied deployments with X-Forwarded-Prefix work correctly
- routes.py: switch to 303 status (POST→GET semantics for the redirect)
- middleware.py: move supports_password check before prefix import to
  avoid unnecessary import on the early-return path
- Add regression tests covering both broken entry points
@zxcasongs
zxcasongs force-pushed the fix/dashboard-auth-password-only-provider branch from 5c78cd0 to 52c3a54 Compare July 7, 2026 03:00
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related to the password-only-provider NotImplementedError crash cluster: issues #55130 / #55498 / #58020 (auto-SSO 500), #55985 (logout crash-loop); fix PRs #54887 (earliest open, middleware-only), #55988 (route 400 guard), #55993 (logout route). This PR is the superset fix — it guards supports_password in both _auto_sso_response (middleware) and auth_login (routes.py), covering the auto-SSO path and the direct /auth/login route in one change. Not a duplicate of any single-site sibling. A human should pick the canonical fix from this cluster.

zxcasongs added 2 commits July 8, 2026 15:10
…y-provider

Resolve conflict in middleware.py: keep PR branch's supports_password
guard (with explanatory comment) over upstream's duplicate.
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the thorough report and for covering both the automatic and direct-login entry points.

Automated hermes-sweeper review found this behavior already implemented on current main:

  • hermes_cli/dashboard_auth/middleware.py:211-213 bypasses auto-SSO for supports_password providers, falling back to the /login credential form.
  • hermes_cli/dashboard_auth/routes.py:195-202 validates next, preserves the forwarded prefix, and redirects password providers from /auth/login to /login before start_login().
  • tests/hermes_cli/test_dashboard_auth_password_login.py:201-217 already covers both the first unauthenticated hit and direct /auth/login path using a provider whose start_login() raises NotImplementedError.
  • These protections landed in 3e24b16f566045399012bc1185fe0cdb6e1a1be9 (fix(dashboard): support mobile OAuth login).

Closing as implemented on main.

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) P2 Medium — degraded but workaround exists sweeper:implemented-on-main Sweeper: behavior already present on current main type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants