@@ -251,72 +251,32 @@ async def _handle_protected_resource_response(self, response: httpx.Response) ->
251251 except ValidationError :
252252 pass
253253
254- def _build_well_known_path (self , pathname : str ) -> str :
255- """Construct well-known path for OAuth metadata discovery."""
256- well_known_path = f"/.well-known/oauth-authorization-server{ pathname } "
257- if pathname .endswith ("/" ):
258- # Strip trailing slash from pathname to avoid double slashes
259- well_known_path = well_known_path [:- 1 ]
260- return well_known_path
261-
262- def _should_attempt_fallback (self , response_status : int , pathname : str ) -> bool :
263- """Determine if fallback to root discovery should be attempted."""
264- return response_status == 404 and pathname != "/"
265-
266- async def _try_metadata_discovery (self , url : str ) -> httpx .Request :
267- """Build metadata discovery request for a specific URL."""
268- return httpx .Request ("GET" , url , headers = {MCP_PROTOCOL_VERSION : LATEST_PROTOCOL_VERSION })
269-
270- async def _discover_oauth_metadata (self ) -> httpx .Request :
271- """Build OAuth metadata discovery request with fallback support."""
272- if self .context .auth_server_url :
273- auth_server_url = self .context .auth_server_url
274- else :
275- auth_server_url = self .context .server_url
276-
277- # Per RFC 8414, try path-aware discovery first
254+ def _get_discovery_urls (self ) -> list [str ]:
255+ """Generate ordered list of (url, type) tuples for discovery attempts."""
256+ urls : list [str ] = []
257+ auth_server_url = self .context .auth_server_url or self .context .server_url
278258 parsed = urlparse (auth_server_url )
279- well_known_path = self ._build_well_known_path (parsed .path )
280259 base_url = f"{ parsed .scheme } ://{ parsed .netloc } "
281- url = urljoin (base_url , well_known_path )
282260
283- # Store fallback info for use in response handler
284- self .context .discovery_base_url = base_url
285- self .context .discovery_pathname = parsed .path
261+ # RFC 8414: Path-aware OAuth discovery
262+ if parsed .path and parsed .path != "/" :
263+ oauth_path = f"/.well-known/oauth-authorization-server{ parsed .path .rstrip ('/' )} "
264+ urls .append (urljoin (base_url , oauth_path ))
286265
287- return await self ._try_metadata_discovery (url )
266+ # OAuth root fallback
267+ urls .append (urljoin (base_url , "/.well-known/oauth-authorization-server" ))
288268
289- async def _discover_oauth_metadata_fallback (self ) -> httpx .Request :
290- """Build fallback OAuth metadata discovery request for legacy servers."""
291- base_url = getattr (self .context , "discovery_base_url" , "" )
292- if not base_url :
293- raise OAuthFlowError ("No base URL available for fallback discovery" )
294-
295- # Fallback to root discovery for legacy servers
296- url = urljoin (base_url , "/.well-known/oauth-authorization-server" )
297- return await self ._try_metadata_discovery (url )
298-
299- async def _handle_oauth_metadata_response (self , response : httpx .Response , is_fallback : bool = False ) -> bool :
300- """Handle OAuth metadata response. Returns True if handled successfully."""
301- if response .status_code == 200 :
302- try :
303- content = await response .aread ()
304- metadata = OAuthMetadata .model_validate_json (content )
305- self .context .oauth_metadata = metadata
306- # Apply default scope if none specified
307- if self .context .client_metadata .scope is None and metadata .scopes_supported is not None :
308- self .context .client_metadata .scope = " " .join (metadata .scopes_supported )
309- return True
310- except ValidationError :
311- pass
269+ # RFC 8414 section 5: Path-aware OIDC discovery
270+ # See https://www.rfc-editor.org/rfc/rfc8414.html#section-5
271+ if parsed .path and parsed .path != "/" :
272+ oidc_path = f"/.well-known/openid-configuration{ parsed .path .rstrip ('/' )} "
273+ urls .append (urljoin (base_url , oidc_path ))
312274
313- # Check if we should attempt fallback (404 on path-aware discovery)
314- if not is_fallback and self ._should_attempt_fallback (
315- response .status_code , getattr (self .context , "discovery_pathname" , "/" )
316- ):
317- return False # Signal that fallback should be attempted
275+ # OIDC 1.0 fallback (appends to full URL per OIDC spec)
276+ oidc_fallback = f"{ auth_server_url .rstrip ('/' )} /.well-known/openid-configuration"
277+ urls .append (oidc_fallback )
318278
319- return True # Signal no fallback needed (either success or non-404 error)
279+ return urls
320280
321281 async def _register_client (self ) -> httpx .Request | None :
322282 """Build registration request or skip if already registered."""
@@ -570,6 +530,17 @@ def _add_auth_header(self, request: httpx.Request) -> None:
570530 if self .context .current_tokens and self .context .current_tokens .access_token :
571531 request .headers ["Authorization" ] = f"Bearer { self .context .current_tokens .access_token } "
572532
533+ def _create_oauth_metadata_request (self , url : str ) -> httpx .Request :
534+ return httpx .Request ("GET" , url , headers = {MCP_PROTOCOL_VERSION : LATEST_PROTOCOL_VERSION })
535+
536+ async def _handle_oauth_metadata_response (self , response : httpx .Response ) -> None :
537+ content = await response .aread ()
538+ metadata = OAuthMetadata .model_validate_json (content )
539+ self .context .oauth_metadata = metadata
540+ # Apply default scope if needed
541+ if self .context .client_metadata .scope is None and metadata .scopes_supported is not None :
542+ self .context .client_metadata .scope = " " .join (metadata .scopes_supported )
543+
573544 async def async_auth_flow (self , request : httpx .Request ) -> AsyncGenerator [httpx .Request , httpx .Response ]:
574545 """HTTPX auth flow integration."""
575546 async with self .context .lock :
@@ -603,15 +574,19 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
603574 await self ._handle_protected_resource_response (discovery_response )
604575
605576 # Step 2: Discover OAuth metadata (with fallback for legacy servers)
606- oauth_request = await self ._discover_oauth_metadata ()
607- oauth_response = yield oauth_request
608- handled = await self ._handle_oauth_metadata_response (oauth_response , is_fallback = False )
609-
610- # If path-aware discovery failed with 404, try fallback to root
611- if not handled :
612- fallback_request = await self ._discover_oauth_metadata_fallback ()
613- fallback_response = yield fallback_request
614- await self ._handle_oauth_metadata_response (fallback_response , is_fallback = True )
577+ discovery_urls = self ._get_discovery_urls ()
578+ for url in discovery_urls :
579+ oauth_metadata_request = self ._create_oauth_metadata_request (url )
580+ oauth_metadata_response = yield oauth_metadata_request
581+
582+ if oauth_metadata_response .status_code == 200 :
583+ try :
584+ await self ._handle_oauth_metadata_response (oauth_metadata_response )
585+ break
586+ except ValidationError :
587+ continue
588+ elif oauth_metadata_response .status_code < 400 or oauth_metadata_response .status_code >= 500 :
589+ break # Non-4XX error, stop trying
615590
616591 # Step 3: Register client if needed
617592 registration_request = await self ._register_client ()
@@ -626,6 +601,6 @@ async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.
626601 logger .exception ("OAuth flow error" )
627602 raise
628603
629- # Retry with new tokens
630- self ._add_auth_header (request )
631- yield request
604+ # Retry with new tokens
605+ self ._add_auth_header (request )
606+ yield request
0 commit comments