Skip to content

[Fix] Replayed MCP OAuth refresh tokens leave the rest of the token family valid - #1237

Merged
mrubens merged 1 commit into
developfrom
fix/mcp-refresh-token-replay-revocation-0v0fmqdabiz5y
Aug 12, 2026
Merged

[Fix] Replayed MCP OAuth refresh tokens leave the rest of the token family valid#1237
mrubens merged 1 commit into
developfrom
fix/mcp-refresh-token-replay-revocation-0v0fmqdabiz5y

Conversation

@roomote-roomote

Copy link
Copy Markdown
Contributor

​Opened on behalf of @mrubens. View the task or mention @roomote-roomote for follow-up asks.

Related issue

Found during an internal security review of the remote MCP OAuth flow shipped in #1219 — no external issue; this is internal Roomote work.

Why this PR exists

  • A maintainer explicitly invited this PR in the linked issue or discussion
  • I am a maintainer / this is internal Roomote work

What changed

Replaying an already-rotated remote MCP OAuth refresh token now revokes the entire session family, per the OAuth 2.0 Security BCP refresh-token reuse-detection guidance. Previously the token route's refresh path called getRemoteMcpRefreshSession() first, which returns null for an already-rotated token, and returned invalid_grant before ever reaching rotateRemoteMcpRefreshToken() — the only place the reuse-detection Lua branch ran. The result: the replayed token was rejected, but the family's current token kept working, so a stolen-token signal never disabled the remaining tokens.

The route now falls through to a new revokeRemoteMcpRefreshSessionOnReplay helper when the presented token does not resolve to an active session. The helper only fires on the rotated:<sessionId> marker left behind by a successful rotation (keyed by the SHA-256 of the presented token), then deletes the session record and its current token hash, killing every descendant token. Unknown, malformed, or still-current tokens cannot revoke a live family.

How it was tested

  • New unit tests: route-level replay triggers family revocation (and never mints/rotates); library-level replay kills the family's current token; current, unknown, and malformed tokens leave a live family intact.
  • Full OAuth suite passes: 28/28 tests across mcp-remote-oauth.test.ts and the token route tests; tsc --noEmit, oxlint, and oxfmt are clean.
  • Verified end-to-end against a live dev stack: rotate → replay the old token → invalid_grant, and the family's current token is then also rejected (previously it exchanged successfully). A garbage-token replay against a separate live family did not affect it.

Checklist

  • The PR title follows the repo convention: [Fix], [Feat], [Improve], [Refactor], [Docs], or [Chore] followed by a user-facing description
  • This PR is small and scoped to one change
  • pnpm lint and pnpm check-types pass locally
  • I added tests or included a clear manual validation note above
  • I removed secrets, tokens, private keys, and customer data from code, logs, and screenshots
  • If this change should appear in the changelog, I ran pnpm changeset

When a presented refresh token no longer resolves to an active session,
check for the rotated: marker left by a prior rotation and revoke the
whole session family (session record plus current token hash), per the
OAuth 2.0 Security BCP. Previously the route's active-session pre-check
returned invalid_grant before the reuse-detection path could run, so a
replayed rotated token was rejected but the family's current token kept
working. Unknown or malformed tokens still cannot revoke a live family.
@roomote-community

roomote-community Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

No code issues found. See task

Reviewed 60a9ac5

@mrubens
mrubens marked this pull request as ready for review August 12, 2026 04:04
@mrubens
mrubens merged commit 3f8c4b6 into develop Aug 12, 2026
18 checks passed
@mrubens
mrubens deleted the fix/mcp-refresh-token-replay-revocation-0v0fmqdabiz5y branch August 12, 2026 04:04
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