Conversation
…clines the token The session cookie carries no provider identity, so _attempt_refresh must offer the refresh token to every registered provider in turn -- like the verify loop already does. It previously returned on the first provider's failure, so in a stacked multi-provider dashboard a session whose RT belonged to a later provider was bounced to /login on every access-token expiry whenever a non-owning provider (foreign RT -> token-endpoint 400 -> RefreshExpiredError) or an unreachable provider (ProviderError) was tried first. Continue past both failure modes; only force re-login when no provider can refresh. Mirrors the verify-loop stacked-provider fix. Adds regression tests for unreachable-first, foreign-RT-first, and the all-decline re-login guard.
|
Verification: refresh-chain parity gap fix looks correct. The What changed: Two Test coverage is thorough:
The all-providers-fail guard ensures we don't silently accept a dead session when no provider can rotate the RT. Clean fix, well-tested. LGTM. |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for identifying the real refresh-chain gap: current main still returns from _attempt_refresh on the first provider failure (hermes_cli/dashboard_auth/middleware.py:441-462).
Problems
- The proposed
continueathermes_cli/dashboard_auth/middleware.py:369/:385sends a bearer refresh token to every later provider. This is not equivalent to access-token verification: the Nous provider transmits the supplied token to its remote endpoint in both request data andx-nous-refresh-token(plugins/dashboard_auth/nous/__init__.py:282-290). A self-hosted provider's RT could therefore be disclosed to Nous when Nous is registered first. - The branch also predates the current
list_session_providers()filtering (hermes_cli/dashboard_auth/middleware.py:438), so it should not restore the olderlist_providers()loop.
Suggested changes
- Bind the issuing provider to the session with an integrity-protected, server-verifiable mechanism, then refresh only that provider. Add a regression proving non-owning providers never receive the RT.
Automated hermes-sweeper review.
| return None | ||
| for provider in list_providers(): | ||
| try: | ||
| new_session = provider.refresh_session(refresh_token=refresh_token) |
There was a problem hiding this comment.
Blocking: this fan-outs a bearer refresh token to each registered provider after a rejection. That is credential disclosure, not just a verification retry: the Nous provider transmits its refresh_token argument to the remote Portal token endpoint (plugins/dashboard_auth/nous/__init__.py:282-290). Persist and validate the issuing provider, then call only that provider's refresh path.
What
_attempt_refreshnow tries every registered auth provider instead ofgiving up on the first one's failure. It continues past both a foreign-RT
rejection (
RefreshExpiredError) and an unreachable provider(
ProviderError), forcing re-login only when no provider can refresh therefresh token.
Why
The session cookie carries no provider identity, so the middleware cannot
know which provider owns the refresh token — it must try each in turn,
exactly like the verify loop. Before this change, a stacked multi-provider
dashboard bounced the user to
/loginon every access-token expiry whenevera non-owning or unreachable provider was tried before the provider that
actually owns the RT.
Mirrors the verify-loop fix in 616c0a3
(fix(dashboard-auth): don't abort verify chain on one provider's
ProviderError) and closes the same gap on the refresh path.
Tests
Added 3 regression tests to
tests/hermes_cli/test_dashboard_auth_middleware.py:All 3 fail on the pre-fix code and pass with the fix.
Result — dashboard-auth gate + provider suite: