Skip to content

fix(auth): let concurrent agent processes refresh one MCP credential - #359

Merged
vishr merged 3 commits into
mainfrom
fix/oauth-refresh-grace
Oct 19, 2026
Merged

vishr merged 3 commits into
mainfrom
fix/oauth-refresh-grace

Conversation

@vishr

@vishr vishr commented Oct 19, 2026

Copy link
Copy Markdown
Member

Codex processes share one stored Fanout login. When two refresh at once, the second presents the refresh token the first just rotated. Reuse detection then revoked the whole family and signed every process out: production logged 35 rejected /oauth/token requests within minutes on 2026-10-19.

The owner approved this trade-off:

  • Grace window. A refresh token rotated within the last 60 seconds is admitted again, but only when the request comes from the same client, for the same resource, while the token is unexpired and its family still has a live refresh token. That sibling gets its own pair in the family, and the rotation time is not moved, so the window cannot be extended. Okta and Auth0 offer the same option, as a "grace period" or "reuse interval".
  • Still reuse, and still revokes the family: any reuse after the window, of an expired rotated token, of a rotation dated in the future (clock stepped back), or against a revoked family (reuse, user revocation, suspension, retired scope).
  • Access tokens last 1 hour instead of 15 minutes. Every request checks them against the database, so revocation stays immediate, and refreshes, and the chance of such races, fall fourfold.
  • Docs: docs/authentication.md updated.

Tests:

  • a sibling within the window gets distinct working credentials;
  • the window cannot be extended, and reuse after it revokes all descendants;
  • another client is refused;
  • a revoked family cannot be revived;
  • a clock step back is treated as reuse;
  • an expired replay within the window is treated as reuse;
  • the existing reuse, cleanup and HTTP refresh tests are adjusted for the window.

Verification:

  • just check passes.
  • Three Codex adversarial security review rounds. Round one found the clock-step-back case and round two found the expiry-crossing case; both are fixed with regression tests. Round three approved.

https://claude.ai/code/session_018HfyEXHyGD1utmKF4CQtyQ

vishr added 3 commits October 19, 2026 12:39
Codex processes share one stored Fanout login. When two refreshed at
once, the second presented the token the first had just rotated, and
reuse detection revoked the whole family, signing every process out
(35 rejected refreshes on production within minutes).

A refresh token rotated within the last minute is now admitted for the
same client and resource while its family is still live, and that
sibling receives its own pair in the family. Its rotation time is not
moved, so the window cannot extend. Later reuse, or reuse against a
revoked family, still revokes the family.

Access tokens now last an hour instead of 15 minutes. Every request
checks them against the database, so revocation remains immediate, and
refreshes, and the chance of such races, fall fourfold.

Claude-Session: https://claude.ai/code/session_018HfyEXHyGD1utmKF4CQtyQ
A clock stepped back after a rotation made the elapsed time negative,
which counted as inside the grace window and could stretch it to the
size of the step. A future-dated rotation is now reuse.

Claude-Session: https://claude.ai/code/session_018HfyEXHyGD1utmKF4CQtyQ
… grace window

A token rotated just before expiry and replayed just after qualified as a
sibling, then failed the expiry check without revoking its family. Only
an unexpired rotated token can be a sibling.

Claude-Session: https://claude.ai/code/session_018HfyEXHyGD1utmKF4CQtyQ
@vishr
vishr force-pushed the fix/oauth-refresh-grace branch from f7430ae to 44f9ddf Compare October 19, 2026 19:39
@vishr
vishr merged commit d133751 into main Oct 19, 2026
17 checks passed
@vishr
vishr deleted the fix/oauth-refresh-grace branch October 19, 2026 19:54
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