|
34 | 34 | logger = logging.getLogger(__name__) |
35 | 35 |
|
36 | 36 |
|
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: |
38 | 38 | """ |
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 |
45 | 43 | """ |
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: |
47 | 49 | return None |
48 | 50 |
|
49 | | - # Look for resource_metadata parameter in the header |
50 | 51 | # Pattern matches: resource_metadata="url" or resource_metadata=url (unquoted) |
51 | 52 | pattern = r'resource_metadata=(?:"([^"]+)"|([^\s,]+))' |
52 | | - match = re.search(pattern, header_value) |
| 53 | + match = re.search(pattern, www_auth_header) |
53 | 54 |
|
54 | 55 | if match: |
55 | 56 | # Return quoted value if present, otherwise unquoted value |
@@ -228,10 +229,15 @@ def __init__( |
228 | 229 | ) |
229 | 230 | self._initialized = False |
230 | 231 |
|
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 | + |
235 | 241 | return httpx.Request("GET", url, headers={MCP_PROTOCOL_VERSION: LATEST_PROTOCOL_VERSION}) |
236 | 242 |
|
237 | 243 | 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. |
535 | 541 | if not self.context.is_token_valid(): |
536 | 542 | try: |
537 | 543 | # 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) |
540 | 546 | discovery_response = yield discovery_request |
541 | 547 | await self._handle_protected_resource_response(discovery_response) |
542 | 548 |
|
|
0 commit comments