modelcontextprotocol / modelcontextprotocol/python-sdk

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

Đang mở
#2,578 6 bình luận 0 reaction 0 người được giao Xem trên GitHub

Chưa có ai nhận issue này.

auth bug fix proposed P2 ready for work
Ngôn ngữ chính
Python
Star
24.3k
Fork
4k
Merge trung bình
1 ngày 1 giờ
Pull request đã merge (30 ngày)
31

Mô tả

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.

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Bắt đầu từ đâu

  1. Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
  2. Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
  3. Fork repository và làm thay đổi trên một nhánh.
  4. Mở pull request có tham chiếu số hiệu của issue.

Hướng nghiên cứu

Bắt đầu trong src/mcp/client/auth/oauth2.py bằng cách lần theo _refresh_token() và get_resource_url(), sau đó kiểm tra shared/auth.py và packages/client/src/client/auth.ts để tìm luồng TypeScript tương ứng. Tái hiện yêu cầu refresh với một thiết lập Entra ID v2.0 và kiểm tra các test OAuth hoặc request fixture hiện có. Được xem là hoàn tất khi hành vi refresh xử lý nhất quán các tham số bị Entra từ chối và việc chuẩn hóa URL trên các SDK bị ảnh hưởng.

Do mô hình lập chỉ mục viết ra từ nội dung của issue.

Đánh giá

Công nghệ
python, typescript
Lĩnh vực
authentication
Loại issue
Lỗi
Độ khó
4/5
Thời gian dự kiến
3-5 ngày
Mức độ hoạt động
Ít trao đổi
Độ rõ ràng
Khá rõ ràng
Mức phù hợp với người mới
55/100

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.