@@ -303,22 +303,19 @@ def __init__(
303303 )
304304 self._initialized = False
305305
306- def _extract_resource_metadata_from_www_auth (self, init_response: httpx.Response) -> str | None:
306+ def _extract_field_from_www_auth (self, init_response: httpx.Response, field_name: str ) -> str | None:
307307 """
308- Extract protected resource metadata URL from WWW-Authenticate header as per RFC9728 .
308+ Extract field from WWW-Authenticate header.
309309
310310 Returns:
311- Resource metadata URL if found in WWW-Authenticate header, None otherwise
311+ Field value if found in WWW-Authenticate header, None otherwise
312312 """
313- if not init_response or init_response.status_code != 401:
314- return None
315-
316313 www_auth_header = init_response.headers.get("WWW-Authenticate")
317314 if not www_auth_header:
318315 return None
319316
320- # Pattern matches: resource_metadata="url " or resource_metadata=url (unquoted)
321- pattern = r'resource_metadata =(?:"([^"]+)"|([^\s,]+))'
317+ # Pattern matches: field_name="value " or field_name=value (unquoted)
318+ pattern = rf'{field_name} =(?:"([^"]+)"|([^\s,]+))'
322319 match = re.search(pattern, www_auth_header)
323320
324321 if match:
@@ -327,6 +324,27 @@ def _extract_resource_metadata_from_www_auth(self, init_response: httpx.Response
327324
328325 return None
329326
327+ def _extract_resource_metadata_from_www_auth(self, init_response: httpx.Response) -> str | None:
328+ """
329+ Extract protected resource metadata URL from WWW-Authenticate header as per RFC9728.
330+
331+ Returns:
332+ Resource metadata URL if found in WWW-Authenticate header, None otherwise
333+ """
334+ if not init_response or init_response.status_code != 401:
335+ return None
336+
337+ return self._extract_field_from_www_auth(init_response, "resource_metadata")
338+
339+ def _extract_scope_from_www_auth(self, init_response: httpx.Response) -> str | None:
340+ """
341+ Extract scope parameter from WWW-Authenticate header as per RFC6750.
342+
343+ Returns:
344+ Scope string if found in WWW-Authenticate header, None otherwise
345+ """
346+ return self._extract_field_from_www_auth(init_response, "scope")
347+
330348 async def _discover_protected_resource(self, init_response: httpx.Response) -> httpx.Request:
331349 # RFC9728: Try to extract resource_metadata URL from WWW-Authenticate header of the initial response
332350 url = self._extract_resource_metadata_from_www_auth(init_response)
@@ -347,8 +365,32 @@ async def _handle_protected_resource_response(self, response: httpx.Response) ->
347365 self.context.protected_resource_metadata = metadata
348366 if metadata.authorization_servers:
349367 self.context.auth_server_url = str(metadata.authorization_servers[0])
368+
350369 except ValidationError:
351370 pass
371+ else:
372+ raise OAuthFlowError(f"Protected Resource Metadata request failed: {response.status_code}")
373+
374+ def _select_scopes(self, init_response: httpx.Response) -> None:
375+ """Select scopes as outlined in the 'Scope Selection Strategy in the MCP spec."""
376+ # Per MCP spec, scope selection priority order:
377+ # 1. Use scope from WWW-Authenticate header (if provided)
378+ # 2. Use all scopes from PRM scopes_supported (if available)
379+ # 3. Omit scope parameter if neither is available
380+ #
381+ www_authenticate_scope = self._extract_scope_from_www_auth(init_response)
382+ if www_authenticate_scope is not None:
383+ # Priority 1: WWW-Authenticate header scope
384+ self.context.client_metadata.scope = www_authenticate_scope
385+ elif (
386+ self.context.protected_resource_metadata is not None
387+ and self.context.protected_resource_metadata.scopes_supported is not None
388+ ):
389+ # Priority 2: PRM scopes_supported
390+ self.context.client_metadata.scope = " ".join(self.context.protected_resource_metadata.scopes_supported)
391+ else:
392+ # Priority 3: Omit scope parameter
393+ self.context.client_metadata.scope = None
352394
353395 # Discovery and registration helpers provided by BaseOAuthProvider
354396
@@ -508,6 +550,17 @@ def _add_auth_header(self, request: httpx.Request) -> None:
508550 if self.context.current_tokens and self.context.current_tokens.access_token:
509551 request.headers["Authorization"] = f"Bearer {self.context.current_tokens.access_token}"
510552
553+ #<<<<<<< main
554+ #=======
555+ def _create_oauth_metadata_request(self, url: str) -> httpx.Request:
556+ return httpx.Request("GET", url, headers={MCP_PROTOCOL_VERSION: LATEST_PROTOCOL_VERSION})
557+
558+ async def _handle_oauth_metadata_response(self, response: httpx.Response) -> None:
559+ content = await response.aread()
560+ metadata = OAuthMetadata.model_validate_json(content)
561+ self.context.oauth_metadata = metadata
562+
563+ #>>>>>>> main
511564 async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.Request, httpx.Response]:
512565 """HTTPX auth flow integration."""
513566 async with self.context.lock:
@@ -540,8 +593,16 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
540593 discovery_response = yield discovery_request
541594 await self._handle_protected_resource_response(discovery_response)
542595
596+ #<<<<<<< main
543597 # Step 2: Discover OAuth metadata (with fallback for legacy servers)
544598 discovery_urls = self._get_discovery_urls(self.context.auth_server_url or self.context.server_url)
599+ #=======
600+ # Step 2: Apply scope selection strategy
601+ self._select_scopes(response)
602+
603+ # Step 3: Discover OAuth metadata (with fallback for legacy servers)
604+ discovery_urls = self._get_discovery_urls()
605+ #>>>>>>> main
545606 for url in discovery_urls:
546607 oauth_metadata_request = self._create_oauth_metadata_request(url)
547608 oauth_metadata_response = yield oauth_metadata_request
@@ -556,17 +617,22 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
556617 elif oauth_metadata_response.status_code < 400 or oauth_metadata_response.status_code >= 500:
557618 break # Non-4XX error, stop trying
558619
620+ #<<<<<<< main
559621 # Step 3: Register client if needed
560622 registration_request = self._create_registration_request(self._metadata)
623+ #=======
624+ # Step 4: Register client if needed
625+ registration_request = await self._register_client()
626+ #>>>>>>> main
561627 if registration_request:
562628 registration_response = yield registration_request
563629 await self._handle_registration_response(registration_response)
564630 self.context.client_info = self._client_info
565631
566- # Step 4 : Perform authorization
632+ # Step 5 : Perform authorization
567633 auth_code, code_verifier = await self._perform_authorization()
568634
569- # Step 5 : Exchange authorization code for tokens
635+ # Step 6 : Exchange authorization code for tokens
570636 token_request = await self._exchange_token(auth_code, code_verifier)
571637 token_response = yield token_request
572638 await self._handle_token_response(token_response)
@@ -577,6 +643,7 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
577643 # Retry with new tokens
578644 self._add_auth_header(request)
579645 yield request
646+ #<<<<<<< main
580647
581648
582649class ClientCredentialsProvider(BaseOAuthProvider):
@@ -852,3 +919,29 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
852919 response = yield request
853920 if response.status_code == 401:
854921 self._current_tokens = None
922+ #=======
923+ elif response.status_code == 403:
924+ # Step 1: Extract error field from WWW-Authenticate header
925+ error = self._extract_field_from_www_auth(response, "error")
926+
927+ # Step 2: Check if we need to step-up authorization
928+ if error == "insufficient_scope":
929+ try:
930+ # Step 2a: Update the required scopes
931+ self._select_scopes(response)
932+
933+ # Step 2b: Perform (re-)authorization
934+ auth_code, code_verifier = await self._perform_authorization()
935+
936+ # Step 2c: Exchange authorization code for tokens
937+ token_request = await self._exchange_token(auth_code, code_verifier)
938+ token_response = yield token_request
939+ await self._handle_token_response(token_response)
940+ except Exception:
941+ logger.exception("OAuth flow error")
942+ raise
943+
944+ # Retry with new tokens
945+ self._add_auth_header(request)
946+ yield request
947+ #>>>>>>> main
0 commit comments