…rejects it
Microsoft Entra ID v2.0 rejects the RFC 8707 resource parameter on
refresh_token grants (AADSTS9010010), so clients silently lost their
session every time the access token expired (~1 hour) and were forced
back through interactive authorization.
The MCP authorization spec requires the resource parameter on token
requests, so the refresh still sends it. When the authorization server
rejects that refresh with HTTP 400 and an error other than invalid_grant,
the client now retries the refresh once without the resource before
falling back to re-authorization. invalid_grant (a dead refresh token) is
not retried because dropping the resource cannot help. Conformant
authorization servers see no change.
Fixes modelcontextprotocol#1587
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011p29dMDsLnn6KDFmsz2PGr
Fixes #1587
Problem
Microsoft Entra ID v2.0 rejects the RFC 8707 resource parameter on refresh_token grants with AADSTS9010010 ("The resource parameter provided in the request doesn't match with the requested scopes"). ClientOAuthProvider.RefreshTokensAsync always sends resource and returns null on any failure. So for an Entra-protected MCP server, the silent refresh fails every time the access token expires (about every hour), and the client goes back through interactive authorization.
Why not just drop resource on refresh
The MCP authorization spec ("Resource Parameter Implementation") requires clients to send resource on token requests, and RFC 8707 §2.2 defines it for refresh grants. The Python SDK hit the same bug (modelcontextprotocol/python-sdk#2578). Several PRs there that removed resource from refresh were closed, and review pushed toward keeping the spec-required behavior and adding a targeted fallback (modelcontextprotocol/python-sdk#2853). This PR takes the same approach, so the SDKs behave the same way.
Change
Tests
TestOAuthServer gets a RejectRefreshWithResourceError switch that simulates Entra (it rejects refresh_token grants that include resource and accepts ones that don't). It also records the resource value of each refresh request. CanAuthenticate_WithTokenRefresh's middleware, which forces a 401, moved into a shared helper, and these tests use it:
To check the new test can fail, I reverted only the ClientOAuthProvider change and kept the test infrastructure. The Entra scenario test failed and the other two still passed.
Local results on Windows (.NET SDK 10.0.302). The solution dotnet build is clean (warnings are errors).
I couldn't test against a live Entra tenant. The simulated server reproduces the documented behavior (400 when resource is present on refresh, success without it).
🤖 Generated with Claude Code
https://claude.ai/code/session_011p29dMDsLnn6KDFmsz2PGr