fix(deps): bump mcp-oauth to the rotation-race branch build (giantswarm#37164)#977
Conversation
3817131 to
09ff543
Compare
…rm#37164) Pull in the mcp-oauth refresh-token rotation-race fix (root cause 2 of giantswarm#37164) via a pseudo-version pinned to the fix branch (giantswarm/mcp-oauth#511), ahead of the tagged v1.1.0 release. go.mod: mcp-oauth v1.0.12 -> v1.0.13-0.20260720074145-d91d0369e965 (branch build). Includes the #511 GetToken provider-token-ref resolution fix, without which muster SSO / token-forwarding / RFC-8693 token-exchange regressed (14 integration scenarios). With it, 'muster test' is 178/178. Signed-off-by: Pau Rosello <pau@giantswarm.io>
09ff543 to
b007590
Compare
…ntswarm#37164) Replace the rotation-race branch pseudo-version with the tagged release now that the fix has shipped upstream. go.mod: mcp-oauth v1.0.13-0.20260720074145-d91d0369e965 (branch build) -> v1.1.1. v1.1.0 ships the full rotation-race fix for giantswarm#37164 root cause 2 (all four slices: shared per-user provider-token store, per-user single-flight refresh lock, all provider-refresh entry points routed through the coordinator, no-orphan reordering) plus the adversarial-review hardening fixes -- the exact change the branch build was pinned ahead of the tag to get. v1.1.1 is the latest patch on top. Verified green: go build/vet, goimports/gofmt, golangci-lint (gosec+goconst, 0 issues), go test -race (all pass), and 'muster test' 178/178. CRDs regenerate from the Go types with zero diff. Signed-off-by: Pau Rosello <pau@giantswarm.io>
…on storm A long-lived SSO session that reconnects after its login-time ID token has expired (e.g. after a pod restart) re-inits its SSO backend connections in initSSOForSession using the ID token from the live request context, but never persisted that token to the OAuth-proxy store. The background re-exchange / forwarding closure (getIDTokenForForwarding) runs on a detached context.Background() and can only read that store, so it found nothing and logged "no subject ID token available for re-exchange", then fell back to the in-process refresher (RefreshSession -> RefreshAccessToken), which rotates the client's mcp refresh token. On a token-exchange backend whose continuous-listen retries every ~1s, this rotated the refresh-token family ~56x/min until two rotations collided and OAuth 2.1 reuse detection revoked the whole family, deauthing the session. Persist the request-context ID token in initSSOForSession so the store stays populated for as long as the session keeps making authenticated requests, which lets the background re-exchange resolve a subject without triggering the refresher. storeIDTokenForSSO already no-ops on empty/unparseable tokens. This is independent of the mcp-oauth provider-token rotation race (giantswarm/giantswarm#37164); a follow-up should stop the background re-exchange from rotating the client-facing refresh token at all (an mcp-oauth provider-only refresh that fires TokenRefreshHandler without rotating).
Latest tagged release; v1.1.1 was one behind.
|
Folded in a second, independent fix (commit Why: an SSO session on graveler still deauthed with this branch deployedInvestigation of the graveler deauth (session revoked at 09:52:43Z, family + all user/client tokens nuked) showed it was not the mcp-oauth provider-token rotation race this branch targets — it's a muster lifecycle gap:
The fix (
|
QuentinBisson
left a comment
There was a problem hiding this comment.
Reviewed the branch as checked out (not just the rendered diff). Sound change, no blocking defects — the one real problem is the description, which is stale and understates what's in the branch.
1. PR description is inaccurate
The body no longer matches the diff:
- It says the pin is the pseudo-version
v1.0.11-0.20260717070146-dbfd1a44d765; the branch is actually on taggedv1.1.2. That's good — it resolves the "replace pseudo-version with a tagged release" follow-up — but the body still lists that follow-up as open. - It states "No muster source changes — mcp-oauth's exported API is unchanged." That's not true: commit
a46fd8ecarries theinitSSOForSessionID-token-persist fix + a 58-line test. Someone reading only the description wouldn't expect a behavioral change in the SSO re-init path.
Please rewrite the body to cover both changes (dep bump and the persist fix), or drop a46fd8e and let the source fix land via its own PR (see 3).
2. Dep bump is clean
mcp-oauth's go.mod hash is unchanged across v1.0.12 → v1.1.2, so no new transitive deps entered. go build ./internal/aggregator/ and the new test both pass against v1.1.2. The Valkey storage-layout deploy note (every user re-auths once, no legacy read-fallback) is a genuine operational risk and is documented well — just make sure it reaches whoever runs the rollout, not only PR readers.
3. Overlap with #981 — coordinate the merge
Commit a46fd8e is the same source change as #981. Landing both independently risks a duplicate/conflicting merge. Decide explicitly: either #981 owns the source fix and this PR is dep-bump-only, or this PR carries both and #981 is closed.
On the persist fix itself (shared with #981)
Correct and safe. On the authAlive reconnect branch, initSSOForSession runs on a detached context.Background(), so persisting the live request-context ID token is exactly what lets getIDTokenForForwarding resolve a subject instead of falling through to the rotating refresher. The external-bearer/OBO path isn't poisoned either — injectExternalIDToken already mirrors the same token under (sessionID, musterIssuer), so re-storing it is idempotent.
Two minor, non-blocking notes left inline.
| // OAuth 2.1 reuse detection revokes the family. Storing it here keeps the | ||
| // store fresh for as long as the session keeps making authenticated | ||
| // requests. storeIDTokenForSSO no-ops on empty/unparseable tokens. | ||
| if sso.tokens.IDToken != "" { |
There was a problem hiding this comment.
Two minor, non-blocking notes:
- This guard duplicates the empty-check already inside
storeIDTokenForSSO, and the test's second subtest exercises the empty case regardless. Harmless, just redundant — fine to keep for explicitness. storeIDTokenForSSOoverwrites the store entry unconditionally (no "only if newer exp" guard), so in principle a request carrying an older ID token could regress a fresher stored entry. In practice the request-context token just passed validation, so this is a low-risk edge rather than a bug — worth a mental note if store-freshness anomalies ever show up.
Separately (can't inline, it's in server.go): SessionCreationHandler now calls storeIDTokenForSSO twice — once inside initSSOForSession, once explicitly right after. Idempotent, but the explicit second call is now dead weight and could be dropped.
There was a problem hiding this comment.
- Empty-token guard — not changed (kept for explicitness, as you suggested).
- Unconditional overwrite / no newer-exp guard — not changed; agreed low-risk edge since the token just validated, noted to check here first if any store-freshness anomaly shows up.
SessionCreationHandlerdouble call — done, dropped the redundantstoreIDTokenForSSOin 135b254.
…Handler initSSOForSession now persists the request-context ID token to the OAuth-proxy store itself (commit a46fd8e), so the explicit storeIDTokenForSSO call right after it in SessionCreationHandler is dead weight. Idempotent either way; removing it keeps a single source of truth for the persist. Addresses review note on #977.
|
Per-point status on the review summary:
|
…ntswarm#37164) Defect #2 of the graveler re-exchange deauth: muster's background SSO re-exchange, when its cached ID token expired, called the OAuth server's RefreshSession — which delegates to RefreshAccessToken and rotates the client-facing mcp refresh token. On a token-exchange backend whose listen stream retries ~1/s this rotated the refresh-token family server-side until the client's own refresh presented a superseded token and OAuth 2.1 reuse detection revoked the whole family (deauth). Reroute the background refresher to mcp-oauth's new provider-only RefreshSessionProvider: it refreshes the upstream (dex) provider token and repopulates the SSO ID token (via TokenRefreshHandler) WITHOUT rotating the client's mcp refresh token. Renamed across the oauthServer interface, the OAuthHTTPServer / LazyOAuthHTTPServer wrappers, and sessionRefresher; added a wiring guard test (TestSessionRefresher_UsesProviderOnlyRefresh). Bumps mcp-oauth to the branch build carrying RefreshSessionProvider (v1.1.2-0.20260720165823-c4f618c5a6b1, giantswarm/mcp-oauth#524) for graveler branch-build validation; will re-pin to the tagged release before merge. Claude-Session: https://claude.ai/code/session_01PC362NB9bbCmGFCv6Goems
…ard (giantswarm#37164)
QuentinBisson
left a comment
There was a problem hiding this comment.
Approving. The muster changes are correct and well-tested: the initSSOForSession persist keeps the proxy store populated on reconnect, and switching the background refresher to the provider-only RefreshSessionProvider removes the client refresh-token rotation that tripped OAuth 2.1 reuse detection (giantswarm#37164). Build/vet/tests pass with -race on the touched packages, and the rename is complete (no bare RefreshSession callers left).
One thing to resolve before this fix is durable, tracking separately rather than blocking:
Re-pin to a released mcp-oauth tag. go.mod currently pins the pseudo-version v1.1.2-0.20260720165823-c4f618c5a6b1, not the v1.1.2 tag. I checked: server/refresh_session_provider.go does not exist in v1.1.2, v1.1.3, or v1.1.4. The provider-only API lives only on the untagged branch commit c4f618c5, which diverged from mainline. Two risks:
- A later routine
go get -uto any current mcp-oauth tag silently dropsRefreshSessionProviderand regresses the deauth fix, since no released tag carries it. - The fix's provenance is a branch commit that can be force-pushed or GC'd.
Please land the provider-only refresh in mcp-oauth main, cut a real tag (e.g. v1.1.5), and bump muster's pin to it. The PR body still says "bump to tagged v1.1.2 (latest tag)", which is not what go.mod pins, so update that too.
Non-blocking: add a CHANGELOG ### Fixed entry for the deauth fix plus a note that the mcp-oauth bump changes the Valkey token layout with no legacy read-fallback (every user re-authenticates once). The "Follow-up (not in this PR)" section of the body describes the provider-only refresh as future work, but this diff already wires it, so that section is stale.
Replaces the branch pseudo-version v1.1.2-0.20260720165823-c4f618c5a6b1 with the released v1.2.0 tag, which carries the provider-only RefreshSessionProvider API on mainline. Removes the provenance risk of pinning an untagged, force-pushable branch commit and prevents a future go get -u from silently dropping the deauth fix. v1.2.0 also pulls newer transitive prometheus deps (client_golang 1.23.2->1.24.0, common, procfs). Build/vet/race-tests pass on internal/aggregator and internal/server.
…n-race # Conflicts: # go.mod # go.sum
…th v1.2.0 Valkey layout note (giantswarm#37164)
What
Fixes the garm re-exchange rotation-storm deauth (giantswarm#37164) with two related muster source changes plus a dependency bump to a released mcp-oauth tag:
a46fd8e) — a long-lived SSO session that reconnects after its login-time ID token expired now persists that token to the OAuth-proxy store, so muster's background SSO re-exchange stops falling back to the client-refresh-token-rotating path.4538c0d) — the background refresher now calls mcp-oauth'sRefreshSessionProvider, which refreshes the upstream (dex) provider token without rotating the client's mcp refresh token.github.com/giantswarm/mcp-oauthv1.0.10→ taggedv1.2.0(fab38e5) — the released tag that carries the provider-onlyRefreshSessionProviderAPI on mainline (it was only on an untagged branch commit before; re-pinning to a real tag removes the provenance risk and prevents a latergo get -ufrom silently dropping the fix).Supersedes #981 (closed).
Why: an SSO session on graveler deauthed with the pre-fix build
A long-lived SSO session that reconnects after its login-time ID token expired (e.g. after a pod restart) re-inits SSO in
initSSOForSessionfrom the live request context, but never persisted that token to the OAuth-proxy store. The background re-exchange closuregetIDTokenForForwardingruns on a detachedcontext.Background()and can only read that store → finds nothing →no subject ID token available for re-exchange→ falls back to the in-process refresherRefreshSession→RefreshAccessToken, which rotates the client's mcp refresh token.On a token-exchange backend (
garm-mcp-kubernetes) whose continuous-listen retries every ~1s, this rotated the refresh-token family ~56×/min for ~15 min until two rotations collided → OAuth 2.1 reuse detection revoked the whole family → deauth. Connect-time exchange kept succeeding the whole time (it reads the live request context), which is why it looked intermittent.The fix
initSSOForSession(via the existingstoreIDTokenForSSO, which no-ops on empty/unparseable tokens). SinceonAuthenticatedre-inits on active requests, the proxy store stays fresh while the session is active, so the background re-exchange resolves a subject from the store instead of falling through. Also drops the now-redundant explicitstoreIDTokenForSSOcall inSessionCreationHandler.RefreshSessionProvider, which repopulates the upstream provider token (firingTokenRefreshHandler) without touching the client's mcp refresh token — removing the rotation hazard itself, not just its trigger.The new mcp-oauth build changes the Valkey provider-token storage layout (single shared entry per user; no legacy read-fallback). Rolling this out:
token:*,refresh:*, and relatedmeta:*/family:*/ user-client set keys under the configured prefix) for a clean cutover. Skipping the flush is safe — leftover keys are never read and just linger until TTL; no false reuse/theft detection.Testing
go build ./...,go vet ./internal/aggregator/ ./internal/server/✅go test -race ./internal/aggregator/ ./internal/server/✅TestInitSSOForSession_PersistsIDToken(+ empty-token no-op) — persist fix.session_refresher_test.goasserts the background refresher delegates toRefreshSessionProvider; no bareRefreshSessioncallers remain.client_golang 1.23.2 → 1.24.0,common,procfs).