@@ -807,6 +807,8 @@ impl AuthorizationManager {
807807 push_candidate ( format ! ( "/.well-known/openid-configuration/{trimmed}" ) ) ;
808808 // 3. OpenID Connect with path appending
809809 push_candidate ( format ! ( "/{trimmed}/.well-known/openid-configuration" ) ) ;
810+ // 4. Canonical OAuth fallback (without path suffix)
811+ push_candidate ( "/.well-known/oauth-authorization-server" . to_string ( ) ) ;
810812 }
811813
812814 candidates
@@ -1605,7 +1607,7 @@ mod tests {
16051607 // Test URL with single path segment: follow spec priority order
16061608 let base_url = Url :: parse ( "https://auth.example.com/tenant1" ) . unwrap ( ) ;
16071609 let urls = AuthorizationManager :: generate_discovery_urls ( & base_url) ;
1608- assert_eq ! ( urls. len( ) , 3 ) ;
1610+ assert_eq ! ( urls. len( ) , 4 ) ;
16091611 assert_eq ! (
16101612 urls[ 0 ] . as_str( ) ,
16111613 "https://auth.example.com/.well-known/oauth-authorization-server/tenant1"
@@ -1618,11 +1620,15 @@ mod tests {
16181620 urls[ 2 ] . as_str( ) ,
16191621 "https://auth.example.com/tenant1/.well-known/openid-configuration"
16201622 ) ;
1623+ assert_eq ! (
1624+ urls[ 3 ] . as_str( ) ,
1625+ "https://auth.example.com/.well-known/oauth-authorization-server"
1626+ ) ;
16211627
16221628 // Test URL with path and trailing slash
16231629 let base_url = Url :: parse ( "https://auth.example.com/v1/mcp/" ) . unwrap ( ) ;
16241630 let urls = AuthorizationManager :: generate_discovery_urls ( & base_url) ;
1625- assert_eq ! ( urls. len( ) , 3 ) ;
1631+ assert_eq ! ( urls. len( ) , 4 ) ;
16261632 assert_eq ! (
16271633 urls[ 0 ] . as_str( ) ,
16281634 "https://auth.example.com/.well-known/oauth-authorization-server/v1/mcp"
@@ -1635,11 +1641,15 @@ mod tests {
16351641 urls[ 2 ] . as_str( ) ,
16361642 "https://auth.example.com/v1/mcp/.well-known/openid-configuration"
16371643 ) ;
1644+ assert_eq ! (
1645+ urls[ 3 ] . as_str( ) ,
1646+ "https://auth.example.com/.well-known/oauth-authorization-server"
1647+ ) ;
16381648
16391649 // Test URL with multiple path segments
16401650 let base_url = Url :: parse ( "https://auth.example.com/tenant1/subtenant" ) . unwrap ( ) ;
16411651 let urls = AuthorizationManager :: generate_discovery_urls ( & base_url) ;
1642- assert_eq ! ( urls. len( ) , 3 ) ;
1652+ assert_eq ! ( urls. len( ) , 4 ) ;
16431653 assert_eq ! (
16441654 urls[ 0 ] . as_str( ) ,
16451655 "https://auth.example.com/.well-known/oauth-authorization-server/tenant1/subtenant"
@@ -1652,6 +1662,10 @@ mod tests {
16521662 urls[ 2 ] . as_str( ) ,
16531663 "https://auth.example.com/tenant1/subtenant/.well-known/openid-configuration"
16541664 ) ;
1665+ assert_eq ! (
1666+ urls[ 3 ] . as_str( ) ,
1667+ "https://auth.example.com/.well-known/oauth-authorization-server"
1668+ ) ;
16551669 }
16561670
16571671 // StateStore and StoredAuthorizationState tests
@@ -1786,6 +1800,25 @@ mod tests {
17861800 }
17871801 }
17881802
1803+ #[ test]
1804+ fn test_discovery_urls_with_path_suffix ( ) {
1805+ // When the base URL has a path suffix (e.g., /mcp), the discovery should
1806+ // eventually fall back to checking /.well-known/oauth-authorization-server
1807+ // at the root, not just /.well-known/oauth-authorization-server/{path}.
1808+ let base_url = Url :: parse ( "https://mcp.example.com/mcp" ) . unwrap ( ) ;
1809+ let urls = AuthorizationManager :: generate_discovery_urls ( & base_url) ;
1810+
1811+ let canonical_oauth_fallback =
1812+ "https://mcp.example.com/.well-known/oauth-authorization-server" ;
1813+
1814+ assert ! (
1815+ urls. iter( ) . any( |u| u. as_str( ) == canonical_oauth_fallback) ,
1816+ "Expected discovery URLs to include canonical OAuth fallback '{}', but got: {:?}" ,
1817+ canonical_oauth_fallback,
1818+ urls. iter( ) . map( |u| u. as_str( ) ) . collect:: <Vec <_>>( )
1819+ ) ;
1820+ }
1821+
17891822 #[ tokio:: test]
17901823 async fn test_custom_state_store_with_authorization_manager ( ) {
17911824 use std:: sync:: atomic:: { AtomicUsize , Ordering } ;
0 commit comments