Skip to content

Commit 0566d13

Browse files
authored
fix(oauth): support suffixed and preffixed well-knonw paths (#459)
Signed-off-by: Huabing Zhao <[email protected]>
1 parent 717ec56 commit 0566d13

File tree

1 file changed

+121
-38
lines changed

1 file changed

+121
-38
lines changed

crates/rmcp/src/transport/auth.rs

Lines changed: 121 additions & 38 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,68 @@ 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?;
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+
}
224262

225-
if response.status() == StatusCode::OK {
263+
// parse metadata
226264
let metadata = response
227265
.json::<AuthorizationMetadata>()
228266
.await
229267
.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
230270
AuthError::MetadataError(format!("Failed to parse metadata: {}", e))
231271
})?;
232272
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);
256274
}
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+
})
257299
}
258300

259301
/// get client id and credentials
@@ -876,3 +918,44 @@ impl OAuthState {
876918
}
877919
}
878920
}
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

Comments
 (0)