@@ -97,7 +97,6 @@ class OAuthContext:
9797 oauth_metadata : OAuthMetadata | None = None
9898 auth_server_url : str | None = None
9999 protocol_version : str | None = None
100- www_authenticate_scope : str | None = None
101100
102101 # Client registration
103102 client_info : OAuthClientInformationFull | None = None
@@ -212,9 +211,6 @@ def _extract_field_from_www_auth(self, init_response: httpx.Response, field_name
212211 Returns:
213212 Field value if found in WWW-Authenticate header, None otherwise
214213 """
215- if not init_response or init_response .status_code != 401 :
216- return None
217-
218214 www_auth_header = init_response .headers .get ("WWW-Authenticate" )
219215 if not www_auth_header :
220216 return None
@@ -236,6 +232,9 @@ def _extract_resource_metadata_from_www_auth(self, init_response: httpx.Response
236232 Returns:
237233 Resource metadata URL if found in WWW-Authenticate header, None otherwise
238234 """
235+ if not init_response or init_response .status_code != 401 :
236+ return None
237+
239238 return self ._extract_field_from_www_auth (init_response , "resource_metadata" )
240239
241240 def _extract_scope_from_www_auth (self , init_response : httpx .Response ) -> str | None :
@@ -268,26 +267,31 @@ async def _handle_protected_resource_response(self, response: httpx.Response) ->
268267 if metadata .authorization_servers :
269268 self .context .auth_server_url = str (metadata .authorization_servers [0 ])
270269
271- # Per MCP spec, scope selection priority order:
272- # 1. Keep client scope if configured
273- # 2. Use scope from WWW-Authenticate header (if provided)
274- # 3. Use all scopes from PRM scopes_supported (if available)
275- # 4. Omit scope parameter if neither is available
276- #
277- # Priority 1: Don't touch if client scope is already configured
278- if self .context .client_metadata .scope is None :
279- if self .context .www_authenticate_scope is not None :
280- # Priority 2: WWW-Authenticate header scope
281- self .context .client_metadata .scope = self .context .www_authenticate_scope
282- elif self .context .protected_resource_metadata .scopes_supported is not None :
283- # Priority 3: PRM scopes_supported
284- self .context .client_metadata .scope = " " .join (
285- self .context .protected_resource_metadata .scopes_supported
286- )
287- # Priority 4: Omit scope parameter
288-
289270 except ValidationError :
290271 pass
272+ else :
273+ raise OAuthFlowError (f"Protected Resource Metadata request failed: { response .status_code } " )
274+
275+ def _configure_scope_selection (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+ # Step 1: Extract scope from WWW-Authenticate header
283+ www_authenticate_scope = self ._extract_scope_from_www_auth (init_response )
284+ if www_authenticate_scope is not None :
285+ # Priority 1: WWW-Authenticate header scope
286+ self .context .client_metadata .scope = www_authenticate_scope
287+ elif self .context .protected_resource_metadata is not None and self .context .protected_resource_metadata .scopes_supported is not None :
288+ # Priority 2: PRM scopes_supported
289+ self .context .client_metadata .scope = " " .join (
290+ self .context .protected_resource_metadata .scopes_supported
291+ )
292+ else :
293+ # Priority 3: Omit scope parameter
294+ self .context .client_metadata .scope = None
291295
292296 def _get_discovery_urls (self ) -> list [str ]:
293297 """Generate ordered list of (url, type) tuples for discovery attempts."""
@@ -544,14 +548,14 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
544548 # Perform full OAuth flow
545549 try :
546550 # OAuth flow must be inline due to generator constraints
547- # Step 1: Extract scope from WWW-Authenticate header
548- self .context .www_authenticate_scope = self ._extract_scope_from_www_auth (response )
549-
550- # Step 2: Discover protected resource metadata (RFC9728 with WWW-Authenticate support)
551+ # Step 1: Discover protected resource metadata (RFC9728 with WWW-Authenticate support)
551552 discovery_request = await self ._discover_protected_resource (response )
552553 discovery_response = yield discovery_request
553554 await self ._handle_protected_resource_response (discovery_response )
554555
556+ # Step 2: Apply scope selection strategy
557+ self ._configure_scope_selection (response )
558+
555559 # Step 3: Discover OAuth metadata (with fallback for legacy servers)
556560 discovery_urls = self ._get_discovery_urls ()
557561 for url in discovery_urls :
@@ -587,3 +591,27 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
587591 # Retry with new tokens
588592 self ._add_auth_header (request )
589593 yield request
594+ elif response .status_code == 403 :
595+ try :
596+ # Step 1: Extract error field from WWW-Authenticate header
597+ error = self ._extract_field_from_www_auth (response , "error" )
598+
599+ # Step 2: Check if we need to step-up authorization
600+ if error == "insufficient_scope" :
601+ # Step 2a: Update the required scopes
602+ self ._configure_scope_selection (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