fix: make OAuth fallback discovery spec-compliant

Follow the MCP spec discovery flow in the fallback path:
1. Fetch Protected Resource Metadata from .well-known/oauth-protected-resource
   (path-specific then root, per RFC 9728)
2. Extract the authorization server URL from authorization_servers
3. Discover Authorization Server Metadata from that URL
   (trying RFC 8414 and OpenID Connect well-known endpoints)

This correctly handles cases where the authorization server is on a
different host from the MCP server, and path-based deployments.
This commit is contained in:
jh-block
2026-04-16 12:18:22 +02:00
parent f8d1c3959b
commit 74c283e2bb
+79 -12
View File
@@ -153,26 +153,93 @@ pub async fn oauth_flow(
Ok(auth_manager)
}
/// Minimal subset of RFC 9728 Protected Resource Metadata, used only in the fallback
/// discovery path. The full type in rmcp is not public.
#[derive(Deserialize)]
struct ProtectedResourceMetadata {
authorization_servers: Option<Vec<String>>,
}
/// Fallback for when OAuthState::start_authorization_with_metadata_url fails due to
/// the resource metadata discovery bug. Fetches OAuth metadata directly from the
/// .well-known/oauth-authorization-server endpoint and creates the session manually.
/// the resource metadata discovery bug. Follows the MCP spec discovery flow:
/// 1. Fetch Protected Resource Metadata from .well-known/oauth-protected-resource
/// 2. Extract the authorization server URL
/// 3. Fetch Authorization Server Metadata from that URL
/// 4. Create the session manually using rmcp's public APIs
async fn start_authorization_with_wellknown_fallback(
mcp_server_url: &str,
redirect_uri: &str,
) -> Result<OAuthState, anyhow::Error> {
let base_url = Url::parse(mcp_server_url)?;
let wellknown_url = format!(
"{}/.well-known/oauth-authorization-server",
base_url.origin().ascii_serialization()
);
let metadata = reqwest::get(&wellknown_url)
.await?
.error_for_status()?
.json::<AuthorizationMetadata>()
.await?;
let origin = base_url.origin().ascii_serialization();
let path = base_url.path().trim_matches('/');
// Step 1: Discover Protected Resource Metadata (RFC 9728)
// Try path-specific endpoint first, then root, per the MCP spec.
let resource_metadata = {
let mut candidates = Vec::new();
if !path.is_empty() {
candidates.push(format!(
"{origin}/.well-known/oauth-protected-resource/{path}"
));
}
candidates.push(format!("{origin}/.well-known/oauth-protected-resource"));
let mut result = None;
for url in &candidates {
if let Ok(resp) = reqwest::get(url).await {
if let Ok(meta) = resp.json::<ProtectedResourceMetadata>().await {
result = Some(meta);
break;
}
}
}
result.ok_or_else(|| anyhow::anyhow!("no protected resource metadata found"))?
};
// Step 2: Extract authorization server URL
let auth_server_url = resource_metadata
.authorization_servers
.as_ref()
.and_then(|servers| servers.first())
.ok_or_else(|| anyhow::anyhow!("no authorization_servers in resource metadata"))?;
// Step 3: Discover Authorization Server Metadata (RFC 8414)
// Try path-qualified and root well-known URLs per the MCP spec.
let auth_server = Url::parse(auth_server_url)?;
let as_origin = auth_server.origin().ascii_serialization();
let as_path = auth_server.path().trim_matches('/');
let as_metadata = {
let mut candidates = Vec::new();
if !as_path.is_empty() {
candidates.push(format!(
"{as_origin}/.well-known/oauth-authorization-server/{as_path}"
));
candidates.push(format!(
"{as_origin}/.well-known/openid-configuration/{as_path}"
));
}
candidates.push(format!(
"{as_origin}/.well-known/oauth-authorization-server"
));
candidates.push(format!("{as_origin}/.well-known/openid-configuration"));
let mut result = None;
for url in &candidates {
if let Ok(resp) = reqwest::get(url).await {
if let Ok(meta) = resp.json::<AuthorizationMetadata>().await {
result = Some(meta);
break;
}
}
}
result.ok_or_else(|| anyhow::anyhow!("no authorization server metadata found"))?
};
// Step 4: Create session using rmcp public APIs
let mut manager = AuthorizationManager::new(mcp_server_url).await?;
manager.set_metadata(metadata);
manager.set_metadata(as_metadata);
let session = AuthorizationSession::new(
manager,