@@ -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,68 @@ 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 ?;
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 ( )
249
+ . await
250
+ {
251
+ Ok ( r) => r,
252
+ Err ( e) => {
253
+ debug ! ( "discovery request failed: {}" , e) ;
254
+ continue ; // try next candidate if request fails
255
+ }
256
+ } ;
257
+
258
+ if response. status ( ) != StatusCode :: OK {
259
+ debug ! ( "discovery returned non-200: {}" , response. status( ) ) ;
260
+ continue ; // try next candidate if response is not OK
261
+ }
224
262
225
- if response . status ( ) == StatusCode :: OK {
263
+ // parse metadata
226
264
let metadata = response
227
265
. json :: < AuthorizationMetadata > ( )
228
266
. await
229
267
. map_err ( |e| {
268
+ // Fail the discovery if we get a 200 but cannot parse the response
269
+ // This indicates a misconfiguration on the server side
230
270
AuthError :: MetadataError ( format ! ( "Failed to parse metadata: {}" , e) )
231
271
} ) ?;
232
272
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 ( )
245
- } ;
246
-
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
- } )
273
+ return Ok ( metadata) ;
256
274
}
275
+
276
+ debug ! ( "No valid .well-known endpoint found, falling back to default endpoints" ) ;
277
+
278
+ // fallback to default endpoints
279
+ let mut auth_base = self . base_url . clone ( ) ;
280
+ // discard the path part, only keep scheme, host, port
281
+ auth_base. set_path ( "" ) ;
282
+
283
+ // Helper function to create endpoint URL
284
+ let create_endpoint = |path : & str | -> String {
285
+ let mut url = auth_base. clone ( ) ;
286
+ url. set_path ( path) ;
287
+ url. to_string ( )
288
+ } ;
289
+
290
+ Ok ( AuthorizationMetadata {
291
+ authorization_endpoint : create_endpoint ( "authorize" ) ,
292
+ token_endpoint : create_endpoint ( "token" ) ,
293
+ registration_endpoint : create_endpoint ( "register" ) ,
294
+ issuer : None ,
295
+ jwks_uri : None ,
296
+ scopes_supported : None ,
297
+ additional_fields : HashMap :: new ( ) ,
298
+ } )
257
299
}
258
300
259
301
/// get client id and credentials
@@ -876,3 +918,44 @@ impl OAuthState {
876
918
}
877
919
}
878
920
}
921
+
922
+ #[ cfg( test) ]
923
+ mod tests {
924
+ use super :: AuthorizationManager ;
925
+
926
+ #[ test]
927
+ fn well_known_paths_root ( ) {
928
+ let paths = AuthorizationManager :: well_known_paths ( "/" , "oauth-authorization-server" ) ;
929
+ assert_eq ! (
930
+ paths,
931
+ vec![ "/.well-known/oauth-authorization-server" . to_string( ) ]
932
+ ) ;
933
+ }
934
+
935
+ #[ test]
936
+ fn well_known_paths_with_suffix ( ) {
937
+ let paths = AuthorizationManager :: well_known_paths ( "/mcp" , "oauth-authorization-server" ) ;
938
+ assert_eq ! (
939
+ paths,
940
+ vec![
941
+ "/.well-known/oauth-authorization-server/mcp" . to_string( ) ,
942
+ "/mcp/.well-known/oauth-authorization-server" . to_string( ) ,
943
+ "/.well-known/oauth-authorization-server" . to_string( ) ,
944
+ ]
945
+ ) ;
946
+ }
947
+
948
+ #[ test]
949
+ fn well_known_paths_trailing_slash ( ) {
950
+ let paths =
951
+ AuthorizationManager :: well_known_paths ( "/v1/mcp/" , "oauth-authorization-server" ) ;
952
+ assert_eq ! (
953
+ paths,
954
+ vec![
955
+ "/.well-known/oauth-authorization-server/v1/mcp" . to_string( ) ,
956
+ "/v1/mcp/.well-known/oauth-authorization-server" . to_string( ) ,
957
+ "/.well-known/oauth-authorization-server" . to_string( ) ,
958
+ ]
959
+ ) ;
960
+ }
961
+ }
0 commit comments