@@ -204,22 +204,19 @@ def __init__(
204204        )
205205        self ._initialized  =  False 
206206
207-     def  _extract_resource_metadata_from_www_auth (self , init_response : httpx .Response ) ->  str  |  None :
207+     def  _extract_field_from_www_auth (self , init_response : httpx .Response ,  field_name :  str ) ->  str  |  None :
208208        """ 
209-         Extract protected resource metadata URL  from WWW-Authenticate header as per RFC9728 . 
209+         Extract field  from WWW-Authenticate header. 
210210
211211        Returns: 
212-             Resource metadata URL  if found in WWW-Authenticate header, None otherwise 
212+             Field value  if found in WWW-Authenticate header, None otherwise 
213213        """ 
214-         if  not  init_response  or  init_response .status_code  !=  401 :
215-             return  None 
216- 
217214        www_auth_header  =  init_response .headers .get ("WWW-Authenticate" )
218215        if  not  www_auth_header :
219216            return  None 
220217
221-         # Pattern matches: resource_metadata="url " or resource_metadata=url  (unquoted) 
222-         pattern  =  r'resource_metadata =(?:"([^"]+)"|([^\s,]+))'
218+         # Pattern matches: field_name="value " or field_name=value  (unquoted) 
219+         pattern  =  rf' { field_name } 
223220        match  =  re .search (pattern , www_auth_header )
224221
225222        if  match :
@@ -228,6 +225,27 @@ def _extract_resource_metadata_from_www_auth(self, init_response: httpx.Response
228225
229226        return  None 
230227
228+     def  _extract_resource_metadata_from_www_auth (self , init_response : httpx .Response ) ->  str  |  None :
229+         """ 
230+         Extract protected resource metadata URL from WWW-Authenticate header as per RFC9728. 
231+ 
232+         Returns: 
233+             Resource metadata URL if found in WWW-Authenticate header, None otherwise 
234+         """ 
235+         if  not  init_response  or  init_response .status_code  !=  401 :
236+             return  None 
237+ 
238+         return  self ._extract_field_from_www_auth (init_response , "resource_metadata" )
239+ 
240+     def  _extract_scope_from_www_auth (self , init_response : httpx .Response ) ->  str  |  None :
241+         """ 
242+         Extract scope parameter from WWW-Authenticate header as per RFC6750. 
243+ 
244+         Returns: 
245+             Scope string if found in WWW-Authenticate header, None otherwise 
246+         """ 
247+         return  self ._extract_field_from_www_auth (init_response , "scope" )
248+ 
231249    async  def  _discover_protected_resource (self , init_response : httpx .Response ) ->  httpx .Request :
232250        # RFC9728: Try to extract resource_metadata URL from WWW-Authenticate header of the initial response 
233251        url  =  self ._extract_resource_metadata_from_www_auth (init_response )
@@ -248,8 +266,32 @@ async def _handle_protected_resource_response(self, response: httpx.Response) ->
248266                self .context .protected_resource_metadata  =  metadata 
249267                if  metadata .authorization_servers :
250268                    self .context .auth_server_url  =  str (metadata .authorization_servers [0 ])
269+ 
251270            except  ValidationError :
252271                pass 
272+         else :
273+             raise  OAuthFlowError (f"Protected Resource Metadata request failed: { response .status_code }  )
274+ 
275+     def  _select_scopes (self , init_response : httpx .Response ) ->  None :
276+         """Select scopes as outlined in the 'Scope Selection Strategy in the MCP spec.""" 
277+         # Per MCP spec, scope selection priority order: 
278+         # 1. Use scope from WWW-Authenticate header (if provided) 
279+         # 2. Use all scopes from PRM scopes_supported (if available) 
280+         # 3. Omit scope parameter if neither is available 
281+         # 
282+         www_authenticate_scope  =  self ._extract_scope_from_www_auth (init_response )
283+         if  www_authenticate_scope  is  not None :
284+             # Priority 1: WWW-Authenticate header scope 
285+             self .context .client_metadata .scope  =  www_authenticate_scope 
286+         elif  (
287+             self .context .protected_resource_metadata  is  not None 
288+             and  self .context .protected_resource_metadata .scopes_supported  is  not None 
289+         ):
290+             # Priority 2: PRM scopes_supported 
291+             self .context .client_metadata .scope  =  " " .join (self .context .protected_resource_metadata .scopes_supported )
292+         else :
293+             # Priority 3: Omit scope parameter 
294+             self .context .client_metadata .scope  =  None 
253295
254296    def  _get_discovery_urls (self ) ->  list [str ]:
255297        """Generate ordered list of (url, type) tuples for discovery attempts.""" 
@@ -478,9 +520,6 @@ async def _handle_oauth_metadata_response(self, response: httpx.Response) -> Non
478520        content  =  await  response .aread ()
479521        metadata  =  OAuthMetadata .model_validate_json (content )
480522        self .context .oauth_metadata  =  metadata 
481-         # Apply default scope if needed 
482-         if  self .context .client_metadata .scope  is  None  and  metadata .scopes_supported  is  not None :
483-             self .context .client_metadata .scope  =  " " .join (metadata .scopes_supported )
484523
485524    async  def  async_auth_flow (self , request : httpx .Request ) ->  AsyncGenerator [httpx .Request , httpx .Response ]:
486525        """HTTPX auth flow integration.""" 
@@ -514,7 +553,10 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
514553                    discovery_response  =  yield  discovery_request 
515554                    await  self ._handle_protected_resource_response (discovery_response )
516555
517-                     # Step 2: Discover OAuth metadata (with fallback for legacy servers) 
556+                     # Step 2: Apply scope selection strategy 
557+                     self ._select_scopes (response )
558+ 
559+                     # Step 3: Discover OAuth metadata (with fallback for legacy servers) 
518560                    discovery_urls  =  self ._get_discovery_urls ()
519561                    for  url  in  discovery_urls :
520562                        oauth_metadata_request  =  self ._create_oauth_metadata_request (url )
@@ -529,16 +571,16 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
529571                        elif  oauth_metadata_response .status_code  <  400  or  oauth_metadata_response .status_code  >=  500 :
530572                            break   # Non-4XX error, stop trying 
531573
532-                     # Step 3 : Register client if needed 
574+                     # Step 4 : Register client if needed 
533575                    registration_request  =  await  self ._register_client ()
534576                    if  registration_request :
535577                        registration_response  =  yield  registration_request 
536578                        await  self ._handle_registration_response (registration_response )
537579
538-                     # Step 4 : Perform authorization 
580+                     # Step 5 : Perform authorization 
539581                    auth_code , code_verifier  =  await  self ._perform_authorization ()
540582
541-                     # Step 5 : Exchange authorization code for tokens 
583+                     # Step 6 : Exchange authorization code for tokens 
542584                    token_request  =  await  self ._exchange_token (auth_code , code_verifier )
543585                    token_response  =  yield  token_request 
544586                    await  self ._handle_token_response (token_response )
@@ -549,3 +591,27 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
549591                # Retry with new tokens 
550592                self ._add_auth_header (request )
551593                yield  request 
594+             elif  response .status_code  ==  403 :
595+                 # Step 1: Extract error field from WWW-Authenticate header 
596+                 error  =  self ._extract_field_from_www_auth (response , "error" )
597+ 
598+                 # Step 2: Check if we need to step-up authorization 
599+                 if  error  ==  "insufficient_scope" :
600+                     try :
601+                         # Step 2a: Update the required scopes 
602+                         self ._select_scopes (response )
603+ 
604+                         # Step 2b: Perform (re-)authorization 
605+                         auth_code , code_verifier  =  await  self ._perform_authorization ()
606+ 
607+                         # Step 2c: Exchange authorization code for tokens 
608+                         token_request  =  await  self ._exchange_token (auth_code , code_verifier )
609+                         token_response  =  yield  token_request 
610+                         await  self ._handle_token_response (token_response )
611+                     except  Exception :
612+                         logger .exception ("OAuth flow error" )
613+                         raise 
614+ 
615+                     # Retry with new tokens 
616+                     self ._add_auth_header (request )
617+                     yield  request 
0 commit comments