Skip to content

fix(auth): compare-and-swap guard on OAuth refresh persist (#4038) - #5294

Merged
diegosouzapw merged 2 commits into
release/v3.8.40from
fix/4038-refresh-cas-guard
Jun 29, 2026
Merged

diegosouzapw merged 2 commits into
release/v3.8.40from
fix/4038-refresh-cas-guard

Conversation

@diegosouzapw

Copy link
Copy Markdown
Collaborator

Summary

Closes part of #4038 (the credential-loss failure mode). Implements the compare-and-swap (CAS) guard on the OAuth refresh persist that #5292 described but did not actually contain — #5292 was closed as unmergeable (a 2,333-file diverged-fork diff with no CAS code in its tree). Full credit to @KooshaPari for the root-cause diagnosis; carried forward here as co-author.

Problem

The per-connection refresh mutex makes [network refresh + DB write] atomic for one connection, but it does not protect against a third writer (a sibling request, a concurrent HealthCheck, or a replica) landing a fresher refresh_token rotation on the same connection_id between the staleness read and the persist. Overwriting that fresher row reverts the sibling's rotation; the next caller loads the now-consumed token, Auth0/Anthropic flag refresh_token_reused, and the whole token family is revoked (the 1352× claude/aa5dd5cf invalidation storm).

Fix

  • runWithCasGuard / getActiveCasGuard AsyncLocalStorage pair (mirrors the existing runWithOnPersist Fix-A pattern). The guard carries the refresh_token the caller presented (the natural CAS version token, since refresh_tokens rotate every refresh) plus a reread of the row's current refresh_token.
  • Inside the per-connection mutex, getAccessToken re-reads via casGuardShouldSkipPersist immediately before effectiveOnPersist; if a concurrent writer rotated the row past the presented token (wasRefreshTokenRotated), the persist is skipped so the DB stays at the fresher state. The caller still receives the new access token — upstream already authenticated the request.
  • chatCore.ts builds the reread from getProviderConnectionById(connectionId) and wraps the reactive 401/403 refresh in runWithCasGuard.
  • Opt-in: no active guard ⇒ byte-identical behavior. Skip/persist counters via getCasGuardStats().

Note: the PR body of #5292 also mentioned a "Bifrost outcome mutex split" — that targets bifrostShadow.ts, which is part of the fork's bifrost architecture and does not exist in OmniRoute, so it is N/A here.

Validation (TDD)

tests/unit/token-refresh-cas-guard-4038.test.ts — 3 cases driving the real getAccessToken with a mocked Anthropic token endpoint + an injected reread:

  1. concurrent rotation ⇒ persist skipped, getCasGuardStats().skipped === 1, caller still gets the access token;
  2. unchanged row ⇒ persist runs, persisted === 1;
  3. no guard ⇒ persist always runs (opt-in, zero behavior change).

Fails without the skip (verified). npm run typecheck:core clean; the existing 55 token-refresh tests (race-comprehensive + service + this) pass; check:file-size OK (baseline bumped with justification).

The per-connection mutex makes [refresh + persist] atomic for one connection, but a
third writer (sibling request / HealthCheck / replica) can land a fresher refresh_token
rotation between the staleness read and the persist. Overwriting it reverts the rotation
→ the next caller loads a consumed token → Auth0/Anthropic revoke the token family (the
1352x claude/aa5dd5cf invalidation storm).

getAccessToken now re-reads the row's current refresh_token immediately before persisting
(inside the mutex) and skips the write when it rotated past the presented token. The caller
still receives the new access token; only the DB overwrite is skipped. Opt-in via
runWithCasGuard (no guard => byte-identical); skip/persist counters via getCasGuardStats().
The comparison reuses wasRefreshTokenRotated. chatCore wires the reread via
getProviderConnectionById. Regression test: token-refresh-cas-guard-4038.test.ts (3 cases,
TDD: fails without the skip).

Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@diegosouzapw
diegosouzapw merged commit 401a7b4 into release/v3.8.40 Jun 29, 2026
7 checks passed
diegosouzapw added a commit that referenced this pull request Jun 29, 2026
…ve value (#5166) (#5304)

Integrated into release/v3.8.40 (CHANGELOG re-resolved against post-#5294 tip; code identical to the green commit)
diegosouzapw added a commit that referenced this pull request Jun 29, 2026
#5231) (#5303)

Integrated into release/v3.8.40 (CHANGELOG re-resolved against post-#5294/#5304 tip; kiro code identical to the green commit)
@diegosouzapw
diegosouzapw deleted the fix/4038-refresh-cas-guard branch June 29, 2026 07:23
tkgo11 pushed a commit to tkgo11/OmniRoute that referenced this pull request Sep 23, 2026
tkgo11 pushed a commit to tkgo11/OmniRoute that referenced this pull request Sep 23, 2026
…ve value (cheaper-inference#5166) (cheaper-inference#5304)

Integrated into release/v3.8.40 (CHANGELOG re-resolved against post-cheaper-inference#5294 tip; code identical to the green commit)
tkgo11 pushed a commit to tkgo11/OmniRoute that referenced this pull request Sep 23, 2026
cheaper-inference#5231) (cheaper-inference#5303)

Integrated into release/v3.8.40 (CHANGELOG re-resolved against post-cheaper-inference#5294/cheaper-inference#5304 tip; kiro code identical to the green commit)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant