@@ -179,6 +179,33 @@ struct AuthorizationState {
179
179
}
180
180
181
181
impl AuthorizationManager {
182
+ fn well_known_paths ( base_path : & str , resource : & str ) -> Vec < String > {
183
+ let trimmed = base_path. trim_start_matches ( '/' ) . trim_end_matches ( '/' ) ;
184
+ let mut candidates = Vec :: new ( ) ;
185
+
186
+ let mut push_candidate = |candidate : String | {
187
+ if !candidates. contains ( & candidate) {
188
+ candidates. push ( candidate) ;
189
+ }
190
+ } ;
191
+
192
+ let canonical = format ! ( "/.well-known/{resource}" ) ;
193
+
194
+ if trimmed. is_empty ( ) {
195
+ push_candidate ( canonical) ;
196
+ return candidates;
197
+ }
198
+
199
+ // This follows the RFC 8414 recommendation for well-known URI discovery
200
+ push_candidate ( format ! ( "{canonical}/{trimmed}" ) ) ;
201
+ // This is a common pattern used by some identity providers
202
+ push_candidate ( format ! ( "/{trimmed}/.well-known/{resource}" ) ) ;
203
+ // The canonical path should always be the last fallback
204
+ push_candidate ( canonical) ;
205
+
206
+ candidates
207
+ }
208
+
182
209
/// create new auth manager with base url
183
210
pub async fn new < U : IntoUrl > ( base_url : U ) -> Result < Self , AuthError > {
184
211
let base_url = base_url. into_url ( ) ?;
@@ -207,53 +234,65 @@ impl AuthorizationManager {
207
234
208
235
/// discover oauth2 metadata
209
236
pub async fn discover_metadata ( & self ) -> Result < AuthorizationMetadata , AuthError > {
210
- // according to the specification, the metadata should be located at "/.well-known/oauth-authorization-server"
211
- let mut discovery_url = self . base_url . clone ( ) ;
212
- let path = discovery_url. path ( ) ;
213
- let path_suffix = if path == "/" { "" } else { path } ;
214
- discovery_url. set_path ( & format ! (
215
- "/.well-known/oauth-authorization-server{path_suffix}"
216
- ) ) ;
217
- debug ! ( "discovery url: {:?}" , discovery_url) ;
218
- let response = self
219
- . http_client
220
- . get ( discovery_url)
221
- . header ( "MCP-Protocol-Version" , "2024-11-05" )
222
- . send ( )
223
- . await ?;
224
-
225
- if response. status ( ) == StatusCode :: OK {
226
- let metadata = response
227
- . json :: < AuthorizationMetadata > ( )
237
+ for candidate_path in
238
+ Self :: well_known_paths ( self . base_url . path ( ) , "oauth-authorization-server" )
239
+ {
240
+ let mut discovery_url = self . base_url . clone ( ) ;
241
+ discovery_url. set_path ( & candidate_path) ;
242
+ debug ! ( "discovery url: {:?}" , discovery_url) ;
243
+
244
+ let response = match self
245
+ . http_client
246
+ . get ( discovery_url)
247
+ . header ( "MCP-Protocol-Version" , "2024-11-05" )
248
+ . send ( )
228
249
. await
229
- . map_err ( |e| {
230
- AuthError :: MetadataError ( format ! ( "Failed to parse metadata: {}" , e) )
231
- } ) ?;
232
- debug ! ( "metadata: {:?}" , metadata) ;
233
- Ok ( metadata)
234
- } else {
235
- // fallback to default endpoints
236
- let mut auth_base = self . base_url . clone ( ) ;
237
- // discard the path part, only keep scheme, host, port
238
- auth_base. set_path ( "" ) ;
239
-
240
- // Helper function to create endpoint URL
241
- let create_endpoint = |path : & str | -> String {
242
- let mut url = auth_base. clone ( ) ;
243
- url. set_path ( path) ;
244
- url. to_string ( )
250
+ {
251
+ Ok ( r) => r,
252
+ Err ( e) => {
253
+ debug ! ( "discovery request failed: {}" , e) ;
254
+ continue ; // try next candidate
255
+ }
245
256
} ;
246
257
247
- Ok ( AuthorizationMetadata {
248
- authorization_endpoint : create_endpoint ( "authorize" ) ,
249
- token_endpoint : create_endpoint ( "token" ) ,
250
- registration_endpoint : create_endpoint ( "register" ) ,
251
- issuer : None ,
252
- jwks_uri : None ,
253
- scopes_supported : None ,
254
- additional_fields : HashMap :: new ( ) ,
255
- } )
258
+ if response. status ( ) == StatusCode :: OK {
259
+ let metadata = response
260
+ . json :: < AuthorizationMetadata > ( )
261
+ . await
262
+ . map_err ( |e| {
263
+ AuthError :: MetadataError ( format ! ( "Failed to parse metadata: {}" , e) )
264
+ } ) ?;
265
+ debug ! ( "metadata: {:?}" , metadata) ;
266
+ return Ok ( metadata) ;
267
+ } else {
268
+ debug ! ( "discovery returned non-200: {}" , response. status( ) ) ;
269
+ continue ; // try next candidate
270
+ }
256
271
}
272
+
273
+ debug ! ( "No valid metadata found, falling back to default endpoints" ) ;
274
+
275
+ // fallback to default endpoints
276
+ let mut auth_base = self . base_url . clone ( ) ;
277
+ // discard the path part, only keep scheme, host, port
278
+ auth_base. set_path ( "" ) ;
279
+
280
+ // Helper function to create endpoint URL
281
+ let create_endpoint = |path : & str | -> String {
282
+ let mut url = auth_base. clone ( ) ;
283
+ url. set_path ( path) ;
284
+ url. to_string ( )
285
+ } ;
286
+
287
+ Ok ( AuthorizationMetadata {
288
+ authorization_endpoint : create_endpoint ( "authorize" ) ,
289
+ token_endpoint : create_endpoint ( "token" ) ,
290
+ registration_endpoint : create_endpoint ( "register" ) ,
291
+ issuer : None ,
292
+ jwks_uri : None ,
293
+ scopes_supported : None ,
294
+ additional_fields : HashMap :: new ( ) ,
295
+ } )
257
296
}
258
297
259
298
/// get client id and credentials
@@ -876,3 +915,45 @@ impl OAuthState {
876
915
}
877
916
}
878
917
}
918
+
919
+ #[ cfg( test) ]
920
+ mod tests {
921
+ use super :: AuthorizationManager ;
922
+
923
+ #[ test]
924
+ fn well_known_paths_root ( ) {
925
+ let paths = AuthorizationManager :: well_known_paths ( "/" , "oauth-authorization-server" ) ;
926
+ assert_eq ! (
927
+ paths,
928
+ vec![ "/.well-known/oauth-authorization-server" . to_string( ) ]
929
+ ) ;
930
+ }
931
+
932
+ #[ test]
933
+ fn well_known_paths_with_suffix ( ) {
934
+ let paths =
935
+ AuthorizationManager :: well_known_paths ( "/mcp" , "oauth-authorization-server" ) ;
936
+ assert_eq ! (
937
+ paths,
938
+ vec![
939
+ "/.well-known/oauth-authorization-server/mcp" . to_string( ) ,
940
+ "/mcp/.well-known/oauth-authorization-server" . to_string( ) ,
941
+ "/.well-known/oauth-authorization-server" . to_string( ) ,
942
+ ]
943
+ ) ;
944
+ }
945
+
946
+ #[ test]
947
+ fn well_known_paths_trailing_slash ( ) {
948
+ let paths =
949
+ AuthorizationManager :: well_known_paths ( "/v1/mcp/" , "oauth-authorization-server" ) ;
950
+ assert_eq ! (
951
+ paths,
952
+ vec![
953
+ "/.well-known/oauth-authorization-server/v1/mcp" . to_string( ) ,
954
+ "/v1/mcp/.well-known/oauth-authorization-server" . to_string( ) ,
955
+ "/.well-known/oauth-authorization-server" . to_string( ) ,
956
+ ]
957
+ ) ;
958
+ }
959
+ }
0 commit comments