Skip to content

feat(mcp): rotate MCP OAuth refresh tokens with replay detection - #8273

Open
andypalmi wants to merge 7 commits into
feat/8271-mcp-audit-attributionfrom
feat/8270-mcp-refresh-token-rotation
Open

feat(mcp): rotate MCP OAuth refresh tokens with replay detection#8273
andypalmi wants to merge 7 commits into
feat/8271-mcp-audit-attributionfrom
feat/8270-mcp-refresh-token-rotation

Conversation

@andypalmi

@andypalmi andypalmi commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Rotates the MCP OAuth refresh token on every refresh, with a grace window and replay detection, replacing the stable (non-rotating) refresh token.

  • Each refresh issues a new refresh token and records the previous one. A rotated-out token presented within a short grace window returns the current tokens, so a concurrent or retried refresh still succeeds.
  • Presented after the window, a rotated-out token is treated as a replay and the grant is revoked so its access tokens stop working and the client must re-authorize (RFC 9700 section 4.14.2).
  • Concurrent refreshes are serialised with a compare-and-swap on the current refresh token: the update only matches while the token is still current, so exactly one refresh rotates and the loser returns the winner's tokens rather than a token that never reached the row.
  • The refresh route defers rotated-token resolution to the controller for MCP clients. It previously rejected any refresh token that was no longer the row's current token, which is the normal case for a rotated MCP token, so the grace window and replay detection never ran through /account/token. A route-level test now drives rotation, grace retry, and replay through the endpoint.

Part of #7423. Closes #8270.

Rotate the refresh token on every MCP refresh instead of keeping it
stable: each refresh issues a new refresh token and records the previous
one. A rotated-out token presented within a short grace window returns
the current tokens, so a concurrent or retried refresh still succeeds;
presented after the window it is treated as a replay and the grant is
revoked (RFC 9700 section 4.14.2).

Concurrent refreshes are serialised with a compare-and-swap on the
current refresh token: the update only matches while the token is still
current, so exactly one refresh rotates and the loser returns the
winner's tokens from the shared cache rather than a token that never
reached the row.
@andypalmi
andypalmi requested a review from cstns August 25, 2026 10:41
@andypalmi andypalmi self-assigned this Aug 25, 2026
@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.22222% with 8 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (feat/8271-mcp-audit-attribution@e09546a). Learn more about missing BASE report.

Files with missing lines Patch % Lines
forge/db/controllers/AccessToken.js 90.62% 3 Missing ⚠️
...20260825-03-add-refresh-token-rotation-tracking.js 62.50% 3 Missing ⚠️
forge/routes/auth/oauth.js 60.00% 2 Missing ⚠️
Additional details and impacted files
@@                        Coverage Diff                         @@
##             feat/8271-mcp-audit-attribution    #8273   +/-   ##
==================================================================
  Coverage                                   ?   76.25%           
==================================================================
  Files                                      ?      451           
  Lines                                      ?    24175           
  Branches                                   ?     6454           
==================================================================
  Hits                                       ?    18435           
  Misses                                     ?     5740           
  Partials                                   ?        0           
Flag Coverage Δ
backend 76.25% <82.22%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

andypalmi added 2 commits August 25, 2026 13:14
The refresh route rejected any refresh token byRefreshToken could not find.
For a rotated MCP token that is the normal case: its hash has moved to
previousRefreshToken, so the guard short-circuited the grace window and
replay detection in refreshToken() before either could run.

Defer resolution to refreshToken() for MCP clients, which handles the
current-or-previous token, the grace window, and replay revocation. Add a
route-level test covering rotation, grace retry, and replay through
/account/token.
@cstns

cstns commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

it's unclear to me what we're fixing here

@cstns

cstns commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

It looks to me that we're treating a symptom before it presented it's effects; could this be postponed to 3.1?

…resh-token-rotation

# Conflicts:
#	forge/db/models/AccessToken.js
@andypalmi

Copy link
Copy Markdown
Contributor Author

Yes, let's postpone this. Agreed it is security hardening rather than something the OAuth flow needs to function, so I'll pull it out of the stack into its own PR and target it separately (3.1 works for me).

For context on what it does: the base refresh flow (exchanging a valid refresh token for a new access token) already lands in #8264 and is complete on its own. This PR adds hardening on top: rotating the refresh token on each use, a compare-and-swap so two concurrent refreshes converge on one result, a short grace window for retries, and replay detection that revokes the grant if an already-rotated refresh token is presented again (RFC 9700 section 4.14.2). None of that is required for refresh to work, which is why it can land later without blocking the rest of the stack.

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.

Rotate MCP OAuth refresh tokens with replay detection

2 participants