Skip to content

fix: retry token refresh without RFC 8707 resource param when the AS rejects it - #2853

Open
fede-kamel wants to merge 2 commits into
modelcontextprotocol:mainfrom
fede-kamel:fix/2578-refresh-resource-param
Open

fix: retry token refresh without RFC 8707 resource param when the AS rejects it#2853
fede-kamel wants to merge 2 commits into
modelcontextprotocol:mainfrom
fede-kamel:fix/2578-refresh-resource-param

Conversation

@fede-kamel

@fede-kamel fede-kamel commented Jun 12, 2026

Copy link
Copy Markdown

Motivation and Context

Fixes #2578.

Microsoft Entra ID v2.0 rejects the RFC 8707 resource parameter on refresh_token grants (AADSTS9010010, strictly enforced since March 2026). Because the SDK sends it on every refresh, MCP clients using Entra OAuth lose authentication after ~1 hour: the silent refresh fails, tokens are cleared, and the user is forced through interactive re-auth every hour.

At the same time, the MCP authorization spec (2025-06-18 and later, "Resource Parameter Implementation") requires the resource parameter in token requests, and RFC 8707 §2.2 defines it for all grant types including refresh. So unconditionally omitting it on refresh (this PR's original approach) would trade the Entra bug for a spec violation — thanks to the review feedback below for pushing on this.

This PR now keeps the spec-required behavior and adds a targeted fallback:

  • The resource parameter is still sent on refresh requests exactly as before.
  • If the refresh fails with HTTP 400 and the error is not invalid_grant, the refresh is retried once without the resource parameter before falling back to full re-authentication.
  • invalid_grant (a dead refresh token) never triggers the retry, since resending without resource cannot help.

For conformant authorization servers nothing changes. For Entra-style servers, the cost is one extra request on an already-failing path, instead of an hourly interactive re-auth.

How Has This Been Tested?

  • tests/client/test_auth.py + tests/interaction/auth/test_lifecycle.py: 155 passed, 1 xfailed (uv run --frozen pytest)
  • New TestRefreshResourceParamFallback covers: the Entra-style 400 → retry without resource → success path; retry failure → full re-auth; invalid_grant → no retry; old protocol version (no resource sent) → no retry
  • Existing spec-conformance tests (resource included on refresh for 2025-06-18+) pass unchanged
  • ruff check and ruff format clean

Breaking Changes

None. Default behavior is unchanged for all conformant authorization servers; the retry only engages after a 400 rejection that is not invalid_grant.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

@fede-kamel

Copy link
Copy Markdown
Author

@maxisbey @Kludex — final maintainer-selection request for #2578. I have just rebased this branch onto current main (e8afa1a), then rerun the focused OAuth surfaces: 153 passed, 1 expected xfail. I also reran the repository test gate after the rebase.\n\nThe issue is still open and there have been multiple overlapping fixes. This PR keeps the change deliberately small: omit resource from refresh_token grants, and normalize only the artificial root trailing slash while preserving meaningful path slashes.\n\nCould you please make a one-line decision on whether this is the implementation you want to take, whether one of the other proposals should be selected instead, or whether the behavior needs a different design? If it is not going to be taken, I will close this PR in 10 days rather than leave duplicate stale work open. Thank you.

@fede-kamel
fede-kamel force-pushed the fix/2578-refresh-resource-param branch from e8afa1a to d32f7c2 Compare August 20, 2026 21:32
@fede-kamel fede-kamel changed the title fix: omit RFC 8707 resource param on refresh_token grants and strip root trailing slash from PRM resource fix: omit RFC 8707 resource param on refresh_token grants Aug 20, 2026
@fede-kamel

Copy link
Copy Markdown
Author

@maxisbey could this get a look? It has been open since June 12 and #2578 still reproduces on today's main: with PRM present and protocol version 2025-06-18, the refresh request body is ['client_id', 'grant_type', 'refresh_token', 'resource'], and Entra ID v2.0 rejects the resource param on refresh_token grants (AADSTS9010010), so Entra-backed MCP clients lose auth every hour.

I've just rebased onto main and slimmed the PR: the trailing-slash half was superseded by #2925 and is dropped, leaving only the refresh-grant fix. 151 auth tests pass, ruff clean.

@keeltrace

This comment was marked as spam.

…rejects it

Keeps the MCP-required resource parameter on refresh_token grants and adds a
one-shot fallback: on a 400 whose error is not invalid_grant, the refresh is
retried once without the resource param before falling back to full re-auth.
Fixes Entra ID v2.0 (AADSTS9010010) interop without violating the MCP
authorization profile.
@fede-kamel
fede-kamel force-pushed the fix/2578-refresh-resource-param branch from d32f7c2 to 69bfdbb Compare August 21, 2026 00:25
@fede-kamel fede-kamel changed the title fix: omit RFC 8707 resource param on refresh_token grants fix: retry token refresh without RFC 8707 resource param when the AS rejects it Aug 21, 2026
@fede-kamel

fede-kamel commented Aug 21, 2026

Copy link
Copy Markdown
Author

@keeltrace you're right, and thanks for the careful read. I checked RFC 8707.

Reworked as of 69bfdbb: the resource param stays on refresh per spec, and on a 400 whose error is not invalid_grant the refresh is retried once without it before falling back to full re-auth. That keeps conformant servers untouched and turns the Entra failure into one extra request instead of an hourly interactive re-auth. Rather than a provider-specific opt-in flag, the fallback is generic — Entra is detected by behavior, not by name — which also covers other non-conformant ASs. New tests cover the retry, the invalid_grant exclusion, and the no-resource-sent case; the pre-existing spec-conformance tests pass unchanged.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 2 files

Re-trigger cubic

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.

OAuth token refresh sends RFC 8707 resource parameter that Entra ID v2.0 rejects (AADSTS9010010)

2 participants