@@ -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