modelcontextprotocol / modelcontextprotocol/python-sdk

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

未关闭
#2,578 6 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

还没有人认领这个 Issue。

auth bug fix proposed P2 ready for work
主要语言
Python
星标
24.3k
派生
4k
平均合并
1 天 1 小时
30 天内合并 PR
31

描述

Problem

The MCP Python SDK sends an RFC 8707 resource parameter on all token requests — including refresh_token grants. Microsoft Entra ID v2.0 rejects this with AADSTS9010010 (The resource parameter provided in the request doesn't match with the requested scopes).

This causes MCP servers using Entra ID OAuth to lose authentication after ~1 hour when the access token expires and the SDK attempts a silent refresh.

Root Cause

Two compounding issues:

1. Entra v2.0 does not support resource on refresh

Entra's v2.0 token endpoint expects scope, not resource. The resource parameter is a v1.0 concept. Since March 2026, Entra strictly validates and rejects resource on token refresh (previously it was silently ignored).

2. Pydantic v2 AnyHttpUrl trailing-slash normalization

ProtectedResourceMetadata.resource is typed as AnyHttpUrl (shared/auth.py:143). When str() is called on a bare-domain URL, Pydantic v2 adds a trailing slash:

>>> str(AnyHttpUrl("https://mcp-server.example.com"))
'https://mcp-server.example.com/'   # trailing slash added

In get_resource_url() (client/auth/oauth2.py:155), this trailing-slash version is used:

prm_resource = str(self.protected_resource_metadata.resource)  # adds trailing slash

But the Entra app registration has the audience as https://mcp-server.example.com (no slash), so the resource and scope audience don't match.

Affected Code

src/mcp/client/auth/oauth2.py:

async def _refresh_token(self) -> httpx.Request:
    refresh_data = {
        "grant_type": "refresh_token",
        "refresh_token": self.context.current_tokens.refresh_token,
        "client_id": self.context.client_info.client_id,
    }
    # This sends 'resource' on refresh — Entra v2.0 rejects it
    if self.context.should_include_resource_param(self.context.protocol_version):
        refresh_data["resource"] = self.context.get_resource_url()  # RFC 8707

The same issue exists in the TypeScript SDK (packages/client/src/client/auth.ts), where WHATWG URL also normalizes bare-domain URLs with a trailing slash.

Suggested Fix

Option A: Strip trailing slash in get_resource_url()
def get_resource_url(self) -> str:
    resource = resource_url_from_server_url(self.server_url)
    if self.protected_resource_metadata and self.protected_resource_metadata.resource:
        prm_resource = str(self.protected_resource_metadata.resource).rstrip('/')
        if check_resource_allowed(requested_resource=resource, configured_resource=prm_resource):
            resource = prm_resource
    return resource
Option B: Include scope alongside resource on refresh

Entra v2.0 tolerates resource if scope is also present and consistent:

refresh_data["scope"] = " ".join(self.context.scopes)
Option C: Make resource on refresh configurable

Allow servers to signal whether the resource parameter should be included on refresh grants, since not all authorization servers support RFC 8707.

Related Issues

Environment

  • MCP Python SDK: v1.27.0
  • Authorization server: Microsoft Entra ID v2.0
  • MCP server: Azure Container Apps with custom EntraTokenVerifier
  • MCP spec version: 2025-06-18 (mandates RFC 8707 resource)

Current Workaround

Server-side: set resource_server_url=None in AuthSettings and do NOT serve /.well-known/oauth-protected-resource metadata. Without PRM, should_include_resource_param() returns False and resource is omitted from refresh requests. Initial auth still works via the WWW-Authenticate header fallback.

贡献指南

打开贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

调研方向

从 src/mcp/client/auth/oauth2.py 开始,跟踪 _refresh_token() 和 get_resource_url(),然后检查 shared/auth.py 和 packages/client/src/client/auth.ts 中对应的 TypeScript 流程。使用 Entra ID v2.0 配置重现 refresh 请求,并检查现有的 OAuth 测试或请求 fixture。完成的标准是:在受影响的 SDK 中,refresh 行为能够一致地处理 Entra 拒绝的参数和 URL 规范化。

由索引模型根据 Issue 内容生成。

评估

技术栈
python, typescript
领域
authentication
Issue 类型
缺陷
难度
4/5
预计耗时
3-5 天
活跃度
冷清
描述清晰度
基本清楚
新手友好度
55/100

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。