@@ -97,7 +97,6 @@ class OAuthContext:
97
97
oauth_metadata : OAuthMetadata | None = None
98
98
auth_server_url : str | None = None
99
99
protocol_version : str | None = None
100
- www_authenticate_scope : str | None = None
101
100
102
101
# Client registration
103
102
client_info : OAuthClientInformationFull | None = None
@@ -212,9 +211,6 @@ def _extract_field_from_www_auth(self, init_response: httpx.Response, field_name
212
211
Returns:
213
212
Field value if found in WWW-Authenticate header, None otherwise
214
213
"""
215
- if not init_response or init_response .status_code != 401 :
216
- return None
217
-
218
214
www_auth_header = init_response .headers .get ("WWW-Authenticate" )
219
215
if not www_auth_header :
220
216
return None
@@ -236,6 +232,9 @@ def _extract_resource_metadata_from_www_auth(self, init_response: httpx.Response
236
232
Returns:
237
233
Resource metadata URL if found in WWW-Authenticate header, None otherwise
238
234
"""
235
+ if not init_response or init_response .status_code != 401 :
236
+ return None
237
+
239
238
return self ._extract_field_from_www_auth (init_response , "resource_metadata" )
240
239
241
240
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) ->
268
267
if metadata .authorization_servers :
269
268
self .context .auth_server_url = str (metadata .authorization_servers [0 ])
270
269
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
-
289
270
except ValidationError :
290
271
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
291
295
292
296
def _get_discovery_urls (self ) -> list [str ]:
293
297
"""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.
544
548
# Perform full OAuth flow
545
549
try :
546
550
# 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)
551
552
discovery_request = await self ._discover_protected_resource (response )
552
553
discovery_response = yield discovery_request
553
554
await self ._handle_protected_resource_response (discovery_response )
554
555
556
+ # Step 2: Apply scope selection strategy
557
+ self ._configure_scope_selection (response )
558
+
555
559
# Step 3: Discover OAuth metadata (with fallback for legacy servers)
556
560
discovery_urls = self ._get_discovery_urls ()
557
561
for url in discovery_urls :
@@ -587,3 +591,27 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
587
591
# Retry with new tokens
588
592
self ._add_auth_header (request )
589
593
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