Skip to content

fix(mcp): handle refresh_token grant in the consent proxy /token endpoint - #2701

Merged
jung-thomas merged 1 commit into
DEVfrom
worktree-mcp-consent-refresh-grant
Oct 8, 2026
Merged

jung-thomas merged 1 commit into
DEVfrom
worktree-mcp-consent-refresh-grant

Conversation

@jung-thomas

Copy link
Copy Markdown
Contributor

What

Adds the refresh_token grant to the consent proxy's /token endpoint. Without it, mcp-remote fails on reconnect with unsupported_grant_type even though the initial auth succeeded.

Live symptom (PROD, 2026-10-08)

Browser consent + IAS login succeeded, initial token issued, transport connected. Then on reconnect: OAuthError2: unsupported_grant_type from refreshAuthorization → executeTokenRequest.

Root cause

handleToken only accepted grant_type=authorization_code. But the discovery doc advertises refresh_token in grant_types_supported, and we pass IAS's refresh_token through to the client verbatim — so the client refreshes against us (its advertised AS). The branch was never implemented → hard 400 on every refresh.

Fix

  • New idpRefresh() boundary (mirrors idpExchange): relays the client's refresh_token to IAS /oauth2/token with the client's own client_id (public client, no secret, no PKCE on refresh).
  • handleToken branches: refresh_token → idpRefresh; authorization_code → unchanged; else → unsupported_grant_type.
  • Passes rotated refresh_token through; falls back to the old one when IAS doesn't rotate.

Tests

+6: refresh happy path, rotation + no-rotation fallback, missing refresh_token (400), IAS-failure (400), and the real idpRefresh body with fetch mocked (the kind of test that caught the last regression). 31 green.

Chain

5th consent fix: #2691 + #2693 + #2696 + #2697 + #2698 + this. After deploy, the full mcp-remote lifecycle (auth → use → token refresh → reconnect) completes.

…oint

Live PROD failure (2026-10-08) after a SUCCESSFUL initial auth: on reconnect
mcp-remote POSTs grant_type=refresh_token to our /token, but handleToken only
accepted authorization_code → 400 "unsupported_grant_type" on every refresh.

The discovery doc advertises refresh_token in grant_types_supported, and we pass
IAS's refresh_token through to the client verbatim at the code exchange — so the
client correctly tries to refresh against us (its advertised AS). We just never
implemented the branch.

Fix: add an idpRefresh() boundary (mirrors idpExchange) that relays the client's
refresh_token to IAS /oauth2/token with grant_type=refresh_token + the client's
own client_id (public client, no secret, no PKCE on refresh). handleToken now
branches: refresh_token → idpRefresh; authorization_code → unchanged; anything
else → unsupported_grant_type. Passes rotated refresh_token through, falls back
to the old one when IAS doesn't rotate.

Tests: +6 (refresh proxy happy path, rotation + no-rotation fallback, missing
refresh_token 400, IAS-failure 400, real idpRefresh body with fetch mocked).
31 unit tests green.
@jung-thomas
jung-thomas marked this pull request as ready for review October 8, 2026 22:24
@jung-thomas
jung-thomas merged commit c7fab4c into DEV Oct 8, 2026
4 checks passed
@jung-thomas
jung-thomas deleted the worktree-mcp-consent-refresh-grant branch October 8, 2026 22:24
jung-thomas added a commit that referenced this pull request Oct 9, 2026
Resolves the 1.37.0 vs 1.37.1 overlap on the two release-managed files:
- .deploy/mta.yaml: keep 1.37.1 (supersedes main's 1.37.0 bump).
- hugo/data/whats_new.json: keep the 757-entry digest, which already
  incorporates main's 752-entry 1.37.0 digest (rebased onto it) plus the
  4 new 1.37.1 PRs (#2700/#2701/#2704/#2705). Verified superset: all
  1.37.0 entries retained, 4 new entries added, no duplicates.
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