context_server: Handle bad WWW-Authenticate resource_metadata URLs (#53502)
In MCP OAuth, when the resource_metadata URL from the WWW-Authenticate header from the MCP server is on the same origin, but points to a broken endpoint (for example Pydantic Logfire doubles the path component, producing /mcp/mcp), fall back to the RFC 9728 well-known URIs instead of failing outright. The header URL is still tried first, as per the MCP spec. Release Notes: - MCP OAuth: Handle bad URLs in WWW-Authenticate by falling back to the well known authorization server metadata URLs.
This commit is contained in:
1 parent
30efea88c3
commit
db2dd21cd3
1 file changed
+78
-1
@@ -694,7 +694,19 @@ pub async fn fetch_protected_resource_metadata(
|
||||
www_authenticate: &WwwAuthenticate,
|
||||
) -> Result<ProtectedResourceMetadata> {
|
||||
let candidate_urls = match &www_authenticate.resource_metadata {
|
||||
Some(url) if url.origin() == server_url.origin() => vec![url.clone()],
|
||||
Some(url) if url.origin() == server_url.origin() => {
|
||||
// Try the header-provided URL first (per MCP spec: "use the resource
|
||||
// metadata URL from the parsed WWW-Authenticate headers when present"),
|
||||
// then fall back to RFC 9728 well-known URIs in case the header URL is
|
||||
// wrong (e.g. a buggy server that doubles the path component).
|
||||
let mut urls = vec![url.clone()];
|
||||
for fallback in protected_resource_metadata_urls(server_url) {
|
||||
if !urls.contains(&fallback) {
|
||||
urls.push(fallback);
|
||||
}
|
||||
}
|
||||
urls
|
||||
}
|
||||
Some(url) => {
|
||||
log::warn!(
|
||||
"Ignoring cross-origin resource_metadata URL {} \
|
||||
@@ -1920,6 +1932,71 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fetch_protected_resource_metadata_falls_back_when_header_url_fails() {
|
||||
// Reproduces the Pydantic Logfire case: the server's WWW-Authenticate
|
||||
// header contains a resource_metadata URL with a doubled path (e.g.
|
||||
// /mcp/mcp), which returns HTML instead of JSON. The client should
|
||||
// fall back to the RFC 9728 well-known URL, which works correctly.
|
||||
gpui::block_on(async {
|
||||
let client = make_fake_http_client(|req| {
|
||||
Box::pin(async move {
|
||||
let uri = req.uri().to_string();
|
||||
if uri
|
||||
== "https://mcp.example.com/.well-known/oauth-protected-resource/api/mcp/mcp"
|
||||
{
|
||||
// Buggy header URL returns HTML (like a SPA catch-all).
|
||||
Ok(Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", "text/html")
|
||||
.body(AsyncBody::from(b"<!doctype html><html></html>".to_vec()))
|
||||
.unwrap())
|
||||
} else if uri
|
||||
== "https://mcp.example.com/.well-known/oauth-protected-resource/api/mcp"
|
||||
{
|
||||
// Correct well-known URL returns valid metadata.
|
||||
json_response(
|
||||
200,
|
||||
r#"{
|
||||
"resource": "https://mcp.example.com/api/mcp",
|
||||
"authorization_servers": ["https://auth.example.com"]
|
||||
}"#,
|
||||
)
|
||||
} else {
|
||||
json_response(404, "{}")
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let server_url = Url::parse("https://mcp.example.com/api/mcp").unwrap();
|
||||
let www_auth = WwwAuthenticate {
|
||||
resource_metadata: Some(
|
||||
// Buggy URL with doubled path component.
|
||||
Url::parse(
|
||||
"https://mcp.example.com/.well-known/oauth-protected-resource/api/mcp/mcp",
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
scope: None,
|
||||
error: None,
|
||||
error_description: None,
|
||||
};
|
||||
|
||||
let metadata = fetch_protected_resource_metadata(&client, &server_url, &www_auth)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
metadata.resource.as_str(),
|
||||
"https://mcp.example.com/api/mcp"
|
||||
);
|
||||
assert_eq!(
|
||||
metadata.authorization_servers[0].as_str(),
|
||||
"https://auth.example.com/"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fetch_protected_resource_metadata_rejects_cross_origin_url() {
|
||||
gpui::block_on(async {
|
||||
|
||||
Reference in new issue
Block a user