fix(desktop): gate OAuth remote connect on AT-or-RT, not access token alone
The desktop OAuth remote-gateway path gated connectivity on hasOauthSessionCookie(), which checks only the access-token cookie (hermes_session_at, ~15 min TTL). The moment that cookie's Max-Age lapsed, Electron's cookie jar dropped it and both resolveRemoteBackend() and sanitizeDesktopConnectionConfig() reported "not signed in" — forcing a full IDP re-login every ~15 min — even though a valid 24h refresh-token cookie (hermes_session_rt) was sitting in the same jar. The desktop OAuth code (2026-06-04) was written against the obsolete "contract v1 issues no refresh token" model, two days after #37247 re-introduced server-side transparent refresh: Portal now issues a 24h rotating, reuse-detected refresh token, and the gateway middleware (_attempt_refresh) rotates a fresh AT from the RT on the next authenticated request. So an expired-AT/live-RT session is fully connectable — the desktop just never let the request through. Fix: - connection-config.cjs: add RT_COOKIE_VARIANTS + cookiesHaveLiveSession() (true when EITHER a live AT or RT cookie is present). Keep cookiesHaveSession() AT-only for callers that need that specific signal. - main.cjs: add hasLiveOauthSession(); resolveRemoteBackend()'s oauth branch now early-outs only when NEITHER cookie is present, otherwise uses the ws-ticket mint as the authoritative liveness probe (that POST carries the RT cookie and triggers the server-side AT rotation). A real 401 still surfaces as needsOauthLogin. Settings indicator + oauth-logout report against the same AT-or-RT notion. - Remove the stale "contract v1 / NO refresh token" docstrings in cookies.py and the verify_session comments in the Nous provider that contradicted #37247. Tests: +57 lines in connection-config.test.cjs covering the RT-only "still connectable" case. node --test: 32/32. dashboard-auth + nous-provider Python suites: 223/223. Note: server-side files (hermes_cli/dashboard_auth/, plugins/dashboard_auth/) are comment/docstring-only here, but this touches outside apps/desktop/ so it needs Teknium review.
This commit is contained in:
@@ -348,9 +348,10 @@ class NousDashboardAuthProvider(DashboardAuthProvider):
|
||||
|
||||
|
||||
def verify_session(self, *, access_token: str) -> Optional[Session]:
|
||||
# Contract: returns None on expiry/invalidity (middleware then
|
||||
# triggers redirect-to-login since refresh_session can never succeed
|
||||
# under V1); raises ProviderError if the IDP is unreachable.
|
||||
# Contract: returns None on expiry/invalidity (the middleware then
|
||||
# tries refresh_session with the RT cookie, falling back to
|
||||
# redirect-to-login if that also fails); raises ProviderError if the
|
||||
# IDP is unreachable.
|
||||
try:
|
||||
claims = self._verify_jwt(access_token)
|
||||
except InvalidCodeError:
|
||||
@@ -359,8 +360,9 @@ class NousDashboardAuthProvider(DashboardAuthProvider):
|
||||
except ProviderError:
|
||||
# JWKS unreachable, etc. Bubble up so middleware emits 503.
|
||||
raise
|
||||
# verify_session has no access to the original refresh_token; pass
|
||||
# "" because in contract V1 there is none anyway.
|
||||
# verify_session validates the AT in isolation and has no access to the
|
||||
# refresh token (it lives in a separate cookie the middleware reads);
|
||||
# pass "" here — the RT-driven rotation path is middleware's job.
|
||||
return self._session_from_claims(access_token, "", claims)
|
||||
|
||||
def revoke_session(self, *, refresh_token: str) -> None:
|
||||
|
||||
Reference in New Issue
Block a user