Auth review follow-ups: Bearer revocation parity, MCP reconnect auth, /auth/me exp discriminator (bd-rk55baiz) - #451
Merged
Conversation
…, MCP reconnect auth, /auth/me exp semantics
Bans and logout-everywhere not_before floors previously only gated
session cookies — a banned user kept full MCP access indefinitely and
a stolen Google ID token survived logout-everywhere for its remaining
lifetime (~1h).
authenticate_claims_for_kind gains a RevocationEnforcement{Enforce,Skip}
parameter; the ledger check (ban -> 403 user_banned, iat < not_before
-> 401 bearer_revoked, missing iat fails closed at anchor 0) runs
between the allowlist check and the auth_ok emission, so a denied
request never logs allow-then-deny. The Bearer dispatch arm enforces;
mint callers (auth_callback/auth_session via authenticate_claims) skip
— bans gate mint explicitly and the min_auth_time clamp keeps
same-second re-login working.
Tests: 6 new integration tests in auth_bearer.rs (5 observed failing
pre-fix); support.rs gains TestHubBuilder::not_before_subs and
ClaimsBuilder::no_iat. E2E: scripts/hub-bearer-revocation-e2e.mjs
drives the real hub binary through the stopped-hub ban/floor procedure
(12/12 checks); full-stack MCP e2e-auth.test.ts green against the
patched hub. Docs: MCP README residual-window paragraph,
session-auth-operations.md, sub_denylist notes in the loopback-PKCE
plan annotated.
…try (bd-l3b1brn8)
A 401/403 on the WS upgrade — or a terminal refresh failure — was
indistinguishable from a network blip: ws folds non-101 upgrades into a
generic error, NodeWebSocketClientAdapter swallowed getBearer failures,
and the retry loop spun silently forever. A revoked grant mid-session
presented as an immortal, silent 'offline'.
sync-client (evidence): the WebSocket factory seam gains an optional
onUpgradeStatus capability. The default ws factory attaches the
EventEmitter-only 'unexpected-response' event and aborts the handshake
itself (ws skips abortHandshake when a listener exists — a no-op
handler leaks one connection per attempt, verified against a raw net
server). The adapter reports definitive evidence only via
onAuthRejected — upgrade 401/403, or a ReauthRequired-named getBearer
error (name-based: the cross-package contract; TokenRefreshError stays
transient) — debounced to one report per failure episode, reset at
peer handshake. Terminal refresh failures stop the retry loop. Network
errors never report and never stop retrying. Also fixed: disconnect()
during a CONNECTING socket crashed the process via ws's unhandled
'closed before established' error event.
hub-mcp (policy): ConnectionManager.handleAuthRejected coalesces
concurrent reports into one forceRefresh+reprobe cycle. Recovery is
silent; persistent 401 invalidates the grant, drops dead project
handles, and gates the next tool call to fail fast with ReauthRequired
(self-heals when fresh credentials appear); 403 — evidence, recheck,
or initial probe — surfaces HubAccessDeniedError ('banned or not in
the allowlist') with the keyring deliberately kept.
Tests: 7 new sync-client specs (incl. real-ws leak pin), 10 new
connection-manager specs, 2 new e2e-auth cases (grant revocation and
mid-session ban via F1's ledger, real hub binary + real keyring).
Suites: sync-client 137, hub-mcp 246, hub-client build:all + 130
tests, q2 embed rebuilt and verified via --launcher-info.
…ect auth classification (F2)
…3sp8) AuthMeResponse.exp meant 'sliding session expiry' on the cookie path but 'fixed Google token expiry' on the Bearer path, with nothing in the response distinguishing them — a latent trap for any Bearer caller that schedules from it. Hub: AuthMeResponse gains credential: "session" | "bearer" (mirroring the AuthenticatedUser variants); exp is documented as the expiry of the presented credential. Additive — exp itself unchanged on both paths. SPA: AuthState carries the discriminator, and the dead pre-sliding DEFAULT_SESSION_MS = 1 h fallback in useAuth is removed — when exp is absent no expiry re-check is scheduled (the mount, visibility-change, keep-alive, and disconnected-probe checks all remain). The fallback was unreachable against a sliding hub and would have probed ~168x too often if it ever fired. Tests: both hub /auth/me integration tests extended with credential assertions (observed failing first); authService mapping test; useAuth fallback-pinned tests rewritten to schedule from an explicit server exp plus a new no-exp -> no-schedule spec (a simulated week, zero re-checks). Full cargo xtask verify green. E2E against the real hub binary: Bearer /auth/me -> credential=bearer with the token's own exp; session cookie -> credential=session with the sliding ~7d exp.
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three fixes from the 2026-08-03 auth review. Full plan and verification record:
claude-notes/plans/2026-08-03-bearer-revocation-and-mcp-auth-followups.md.1. Bans and "log out everywhere" now apply to MCP/CLI clients too (bd-jkih1ql7).
Before: banning a user or logging out everywhere only killed browser sessions — an MCP client (or a stolen Google ID token) kept working, for up to an hour or indefinitely. Now the hub checks its revocation list on every Bearer-token request: banned users are refused outright, and tokens issued before a "log out everywhere" are rejected. Logging in again immediately afterwards still works, exactly as in the browser.
2. MCP clients now explain auth failures instead of silently retrying forever (bd-l3b1brn8).
Before: if the hub rejected an MCP client mid-session (revoked credentials, ban), the client just looked "offline" and retried forever — indistinguishable from a network outage. Now it recognizes a definite rejection: it refreshes the token and tries once more, and if that doesn't help, the next tool call fails immediately with a clear message — either "please re-authenticate" or "your account is not allowed on this hub". Genuine network problems behave as before: keep retrying, never log out.
3.
/auth/menow says what kind of credential its expiry refers to (bd-aw8f3sp8).Before: the
expfield meant a sliding session expiry for browser cookies but a fixed token expiry for Bearer tokens, and nothing told callers which they got. Now the response includescredential: "session"or"bearer", and the web client no longer falls back to a guessed 1-hour lifetime when no expiry is reported.All three were developed test-first, pass the full
cargo xtask verify, and were exercised end-to-end against the realhubbinary and the realq2 mcpclient (ban, log-out-everywhere, and mid-session revocation scenarios).