@@ -687,16 +687,40 @@ impl AuthorizationManager {
687687 }
688688 }
689689
690+ /// Generate discovery endpoint URLs following the priority order in spec-2025-11-25 4.3 "Authorization Server Metadata Discovery".
691+ fn generate_discovery_urls ( base_url : & Url ) -> Vec < Url > {
692+ let mut candidates = Vec :: new ( ) ;
693+ let path = base_url. path ( ) ;
694+ let trimmed = path. trim_start_matches ( '/' ) . trim_end_matches ( '/' ) ;
695+ let mut push_candidate = |discovery_path : String | {
696+ let mut discovery_url = base_url. clone ( ) ;
697+ discovery_url. set_query ( None ) ;
698+ discovery_url. set_fragment ( None ) ;
699+ discovery_url. set_path ( & discovery_path) ;
700+ candidates. push ( discovery_url) ;
701+ } ;
702+ if trimmed. is_empty ( ) {
703+ // No path components: try OAuth first, then OpenID Connect
704+ push_candidate ( "/.well-known/oauth-authorization-server" . to_string ( ) ) ;
705+ push_candidate ( "/.well-known/openid-configuration" . to_string ( ) ) ;
706+ } else {
707+ // Path components present: follow spec priority order
708+ // 1. OAuth 2.0 with path insertion
709+ push_candidate ( format ! ( "/.well-known/oauth-authorization-server/{trimmed}" ) ) ;
710+ // 2. OpenID Connect with path insertion
711+ push_candidate ( format ! ( "/.well-known/openid-configuration/{trimmed}" ) ) ;
712+ // 3. OpenID Connect with path appending
713+ push_candidate ( format ! ( "/{trimmed}/.well-known/openid-configuration" ) ) ;
714+ }
715+
716+ candidates
717+ }
718+
690719 async fn try_discover_oauth_server (
691720 & self ,
692721 base_url : & Url ,
693722 ) -> Result < Option < AuthorizationMetadata > , AuthError > {
694- for candidate_path in Self :: well_known_paths ( base_url. path ( ) , "oauth-authorization-server" )
695- {
696- let mut discovery_url = base_url. clone ( ) ;
697- discovery_url. set_query ( None ) ;
698- discovery_url. set_fragment ( None ) ;
699- discovery_url. set_path ( & candidate_path) ;
723+ for discovery_url in Self :: generate_discovery_urls ( base_url) {
700724 if let Some ( metadata) = self . fetch_authorization_metadata ( & discovery_url) . await ? {
701725 return Ok ( Some ( metadata) ) ;
702726 }
@@ -1460,4 +1484,71 @@ mod tests {
14601484 ]
14611485 ) ;
14621486 }
1487+
1488+ #[ test]
1489+ fn generate_discovery_urls ( ) {
1490+ // Test root URL (no path components): OAuth first, then OpenID Connect
1491+ let base_url = Url :: parse ( "https://auth.example.com" ) . unwrap ( ) ;
1492+ let urls = AuthorizationManager :: generate_discovery_urls ( & base_url) ;
1493+ assert_eq ! ( urls. len( ) , 2 ) ;
1494+ assert_eq ! (
1495+ urls[ 0 ] . as_str( ) ,
1496+ "https://auth.example.com/.well-known/oauth-authorization-server"
1497+ ) ;
1498+ assert_eq ! (
1499+ urls[ 1 ] . as_str( ) ,
1500+ "https://auth.example.com/.well-known/openid-configuration"
1501+ ) ;
1502+
1503+ // Test URL with single path segment: follow spec priority order
1504+ let base_url = Url :: parse ( "https://auth.example.com/tenant1" ) . unwrap ( ) ;
1505+ let urls = AuthorizationManager :: generate_discovery_urls ( & base_url) ;
1506+ assert_eq ! ( urls. len( ) , 3 ) ;
1507+ assert_eq ! (
1508+ urls[ 0 ] . as_str( ) ,
1509+ "https://auth.example.com/.well-known/oauth-authorization-server/tenant1"
1510+ ) ;
1511+ assert_eq ! (
1512+ urls[ 1 ] . as_str( ) ,
1513+ "https://auth.example.com/.well-known/openid-configuration/tenant1"
1514+ ) ;
1515+ assert_eq ! (
1516+ urls[ 2 ] . as_str( ) ,
1517+ "https://auth.example.com/tenant1/.well-known/openid-configuration"
1518+ ) ;
1519+
1520+ // Test URL with path and trailing slash
1521+ let base_url = Url :: parse ( "https://auth.example.com/v1/mcp/" ) . unwrap ( ) ;
1522+ let urls = AuthorizationManager :: generate_discovery_urls ( & base_url) ;
1523+ assert_eq ! ( urls. len( ) , 3 ) ;
1524+ assert_eq ! (
1525+ urls[ 0 ] . as_str( ) ,
1526+ "https://auth.example.com/.well-known/oauth-authorization-server/v1/mcp"
1527+ ) ;
1528+ assert_eq ! (
1529+ urls[ 1 ] . as_str( ) ,
1530+ "https://auth.example.com/.well-known/openid-configuration/v1/mcp"
1531+ ) ;
1532+ assert_eq ! (
1533+ urls[ 2 ] . as_str( ) ,
1534+ "https://auth.example.com/v1/mcp/.well-known/openid-configuration"
1535+ ) ;
1536+
1537+ // Test URL with multiple path segments
1538+ let base_url = Url :: parse ( "https://auth.example.com/tenant1/subtenant" ) . unwrap ( ) ;
1539+ let urls = AuthorizationManager :: generate_discovery_urls ( & base_url) ;
1540+ assert_eq ! ( urls. len( ) , 3 ) ;
1541+ assert_eq ! (
1542+ urls[ 0 ] . as_str( ) ,
1543+ "https://auth.example.com/.well-known/oauth-authorization-server/tenant1/subtenant"
1544+ ) ;
1545+ assert_eq ! (
1546+ urls[ 1 ] . as_str( ) ,
1547+ "https://auth.example.com/.well-known/openid-configuration/tenant1/subtenant"
1548+ ) ;
1549+ assert_eq ! (
1550+ urls[ 2 ] . as_str( ) ,
1551+ "https://auth.example.com/tenant1/subtenant/.well-known/openid-configuration"
1552+ ) ;
1553+ }
14631554}
0 commit comments