Skip to content

Commit 47a435f

Browse files
committed
support suffixed and preffixed well-knonw paths
Signed-off-by: Huabing Zhao <[email protected]>
1 parent 717ec56 commit 47a435f

File tree

1 file changed

+110
-46
lines changed

1 file changed

+110
-46
lines changed

crates/rmcp/src/transport/auth.rs

Lines changed: 110 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,33 @@ struct AuthorizationState {
179179
}
180180

181181
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+
182209
/// create new auth manager with base url
183210
pub async fn new<U: IntoUrl>(base_url: U) -> Result<Self, AuthError> {
184211
let base_url = base_url.into_url()?;
@@ -207,53 +234,53 @@ impl AuthorizationManager {
207234

208235
/// discover oauth2 metadata
209236
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>()
228-
.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()
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-
})
237+
for candidate_path in Self::well_known_paths(self.base_url.path(), "oauth-authorization-server") {
238+
let mut discovery_url = self.base_url.clone();
239+
discovery_url.set_path(&candidate_path);
240+
debug!("discovery url: {:?}", discovery_url);
241+
242+
let response = self
243+
.http_client
244+
.get(discovery_url)
245+
.header("MCP-Protocol-Version", "2024-11-05")
246+
.send()
247+
.await?;
248+
249+
if response.status() == StatusCode::OK {
250+
let metadata = response
251+
.json::<AuthorizationMetadata>()
252+
.await
253+
.map_err(|e| {
254+
AuthError::MetadataError(format!("Failed to parse metadata: {}", e))
255+
})?;
256+
debug!("metadata: {:?}", metadata);
257+
return Ok(metadata);
258+
}
256259
}
260+
261+
debug!("No valid metadata found, falling back to default endpoints");
262+
263+
// fallback to default endpoints
264+
let mut auth_base = self.base_url.clone();
265+
// discard the path part, only keep scheme, host, port
266+
auth_base.set_path("");
267+
268+
// Helper function to create endpoint URL
269+
let create_endpoint = |path: &str| -> String {
270+
let mut url = auth_base.clone();
271+
url.set_path(path);
272+
url.to_string()
273+
};
274+
275+
Ok(AuthorizationMetadata {
276+
authorization_endpoint: create_endpoint("authorize"),
277+
token_endpoint: create_endpoint("token"),
278+
registration_endpoint: create_endpoint("register"),
279+
issuer: None,
280+
jwks_uri: None,
281+
scopes_supported: None,
282+
additional_fields: HashMap::new(),
283+
})
257284
}
258285

259286
/// get client id and credentials
@@ -876,3 +903,40 @@ impl OAuthState {
876903
}
877904
}
878905
}
906+
907+
#[cfg(test)]
908+
mod tests {
909+
use super::AuthorizationManager;
910+
911+
#[test]
912+
fn well_known_paths_root() {
913+
let paths = AuthorizationManager::well_known_paths("/", "oauth-authorization-server");
914+
assert_eq!(paths, vec!["/.well-known/oauth-authorization-server".to_string()]);
915+
}
916+
917+
#[test]
918+
fn well_known_paths_with_suffix() {
919+
let paths = AuthorizationManager::well_known_paths("/mcp", "oauth-authorization-server");
920+
assert_eq!(
921+
paths,
922+
vec![
923+
"/.well-known/oauth-authorization-server/mcp".to_string(),
924+
"/mcp/.well-known/oauth-authorization-server".to_string(),
925+
"/.well-known/oauth-authorization-server".to_string(),
926+
]
927+
);
928+
}
929+
930+
#[test]
931+
fn well_known_paths_trailing_slash() {
932+
let paths = AuthorizationManager::well_known_paths("/mcp/v1/", "oauth-authorization-server");
933+
assert_eq!(
934+
paths,
935+
vec![
936+
"/.well-known/oauth-authorization-server/mcp/v1".to_string(),
937+
"/mcp/v1/.well-known/oauth-authorization-server".to_string(),
938+
"/.well-known/oauth-authorization-server".to_string(),
939+
]
940+
);
941+
}
942+
}

0 commit comments

Comments
 (0)