Skip to content

Commit dd0902e

Browse files
committed
Check for WWW-Authenticate header
1 parent 41b3d91 commit dd0902e

File tree

1 file changed

+22
-16
lines changed

1 file changed

+22
-16
lines changed

src/mcp/client/auth.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,23 @@
3434
logger = logging.getLogger(__name__)
3535

3636

37-
def _extract_resource_metadata_from_www_auth(header_value: str) -> str | None:
37+
def _extract_resource_metadata_from_www_auth(response: httpx.Response) -> str | None:
3838
"""
39-
Parse WWW-Authenticate header to extract resource_metadata parameter.
40-
41-
According to RFC9728, the header format is:
42-
WWW-Authenticate: Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource"
43-
44-
Returns the resource_metadata URL if found, None otherwise.
39+
Extract protected resource metadata URL from WWW-Authenticate header as per RFC9728.
40+
41+
Returns:
42+
Resource metadata URL if found in WWW-Authenticate header, None otherwise
4543
"""
46-
if not header_value:
44+
if not response or response.status_code != 401:
45+
return None
46+
47+
www_auth_header = response.headers.get("WWW-Authenticate")
48+
if not www_auth_header:
4749
return None
4850

49-
# Look for resource_metadata parameter in the header
5051
# Pattern matches: resource_metadata="url" or resource_metadata=url (unquoted)
5152
pattern = r'resource_metadata=(?:"([^"]+)"|([^\s,]+))'
52-
match = re.search(pattern, header_value)
53+
match = re.search(pattern, www_auth_header)
5354

5455
if match:
5556
# Return quoted value if present, otherwise unquoted value
@@ -228,10 +229,15 @@ def __init__(
228229
)
229230
self._initialized = False
230231

231-
async def _discover_protected_resource(self) -> httpx.Request:
232-
"""Build discovery request for protected resource metadata."""
233-
auth_base_url = self.context.get_authorization_base_url(self.context.server_url)
234-
url = urljoin(auth_base_url, "/.well-known/oauth-protected-resource")
232+
async def _discover_protected_resource(self, response: httpx.Response | None = None) -> httpx.Request:
233+
# RFC9728: Try to extract resource_metadata URL from WWW-Authenticate header
234+
url = _extract_resource_metadata_from_www_auth(response) if response else None
235+
236+
if not url:
237+
# Fallback to well-known discovery
238+
auth_base_url = self.context.get_authorization_base_url(self.context.server_url)
239+
url = urljoin(auth_base_url, "/.well-known/oauth-protected-resource")
240+
235241
return httpx.Request("GET", url, headers={MCP_PROTOCOL_VERSION: LATEST_PROTOCOL_VERSION})
236242

237243
async def _handle_protected_resource_response(self, response: httpx.Response) -> None:
@@ -535,8 +541,8 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
535541
if not self.context.is_token_valid():
536542
try:
537543
# OAuth flow must be inline due to generator constraints
538-
# Step 1: Discover protected resource metadata (spec revision 2025-06-18)
539-
discovery_request = await self._discover_protected_resource()
544+
# Step 1: Discover protected resource metadata (RFC9728 with WWW-Authenticate support)
545+
discovery_request = await self._discover_protected_resource(response)
540546
discovery_response = yield discovery_request
541547
await self._handle_protected_resource_response(discovery_response)
542548

0 commit comments

Comments
 (0)