Skip to content

Commit 61f7b7b

Browse files
authored
fix: add OpenID Connect discovery support per spec-2025-11-25 4.3 (#598)
* fix: add OpenID Connect discovery support per spec-2025-11-25 4.3 Previously only tried OAuth 2.0 endpoints. Now tries OAuth first, then OpenID Connect Discovery 1.0 in the spec-mandated priority order. Signed-off-by: tanish111 <[email protected]> * fix: format auth.rs test assertions Reformat assert_eq! statements to satisfy rustfmt checks in CI. Signed-off-by: tanish111 <[email protected]> --------- Signed-off-by: tanish111 <[email protected]>
1 parent 63d89b1 commit 61f7b7b

File tree

1 file changed

+97
-6
lines changed

1 file changed

+97
-6
lines changed

crates/rmcp/src/transport/auth.rs

Lines changed: 97 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -687,16 +687,40 @@ impl AuthorizationManager {
687687
}
688688
}
689689

690+
/// Generate discovery endpoint URLs following the priority order in spec-2025-11-25 4.3 "Authorization Server Metadata Discovery".
691+
fn generate_discovery_urls(base_url: &Url) -> Vec<Url> {
692+
let mut candidates = Vec::new();
693+
let path = base_url.path();
694+
let trimmed = path.trim_start_matches('/').trim_end_matches('/');
695+
let mut push_candidate = |discovery_path: String| {
696+
let mut discovery_url = base_url.clone();
697+
discovery_url.set_query(None);
698+
discovery_url.set_fragment(None);
699+
discovery_url.set_path(&discovery_path);
700+
candidates.push(discovery_url);
701+
};
702+
if trimmed.is_empty() {
703+
// No path components: try OAuth first, then OpenID Connect
704+
push_candidate("/.well-known/oauth-authorization-server".to_string());
705+
push_candidate("/.well-known/openid-configuration".to_string());
706+
} else {
707+
// Path components present: follow spec priority order
708+
// 1. OAuth 2.0 with path insertion
709+
push_candidate(format!("/.well-known/oauth-authorization-server/{trimmed}"));
710+
// 2. OpenID Connect with path insertion
711+
push_candidate(format!("/.well-known/openid-configuration/{trimmed}"));
712+
// 3. OpenID Connect with path appending
713+
push_candidate(format!("/{trimmed}/.well-known/openid-configuration"));
714+
}
715+
716+
candidates
717+
}
718+
690719
async fn try_discover_oauth_server(
691720
&self,
692721
base_url: &Url,
693722
) -> Result<Option<AuthorizationMetadata>, AuthError> {
694-
for candidate_path in Self::well_known_paths(base_url.path(), "oauth-authorization-server")
695-
{
696-
let mut discovery_url = base_url.clone();
697-
discovery_url.set_query(None);
698-
discovery_url.set_fragment(None);
699-
discovery_url.set_path(&candidate_path);
723+
for discovery_url in Self::generate_discovery_urls(base_url) {
700724
if let Some(metadata) = self.fetch_authorization_metadata(&discovery_url).await? {
701725
return Ok(Some(metadata));
702726
}
@@ -1460,4 +1484,71 @@ mod tests {
14601484
]
14611485
);
14621486
}
1487+
1488+
#[test]
1489+
fn generate_discovery_urls() {
1490+
// Test root URL (no path components): OAuth first, then OpenID Connect
1491+
let base_url = Url::parse("https://auth.example.com").unwrap();
1492+
let urls = AuthorizationManager::generate_discovery_urls(&base_url);
1493+
assert_eq!(urls.len(), 2);
1494+
assert_eq!(
1495+
urls[0].as_str(),
1496+
"https://auth.example.com/.well-known/oauth-authorization-server"
1497+
);
1498+
assert_eq!(
1499+
urls[1].as_str(),
1500+
"https://auth.example.com/.well-known/openid-configuration"
1501+
);
1502+
1503+
// Test URL with single path segment: follow spec priority order
1504+
let base_url = Url::parse("https://auth.example.com/tenant1").unwrap();
1505+
let urls = AuthorizationManager::generate_discovery_urls(&base_url);
1506+
assert_eq!(urls.len(), 3);
1507+
assert_eq!(
1508+
urls[0].as_str(),
1509+
"https://auth.example.com/.well-known/oauth-authorization-server/tenant1"
1510+
);
1511+
assert_eq!(
1512+
urls[1].as_str(),
1513+
"https://auth.example.com/.well-known/openid-configuration/tenant1"
1514+
);
1515+
assert_eq!(
1516+
urls[2].as_str(),
1517+
"https://auth.example.com/tenant1/.well-known/openid-configuration"
1518+
);
1519+
1520+
// Test URL with path and trailing slash
1521+
let base_url = Url::parse("https://auth.example.com/v1/mcp/").unwrap();
1522+
let urls = AuthorizationManager::generate_discovery_urls(&base_url);
1523+
assert_eq!(urls.len(), 3);
1524+
assert_eq!(
1525+
urls[0].as_str(),
1526+
"https://auth.example.com/.well-known/oauth-authorization-server/v1/mcp"
1527+
);
1528+
assert_eq!(
1529+
urls[1].as_str(),
1530+
"https://auth.example.com/.well-known/openid-configuration/v1/mcp"
1531+
);
1532+
assert_eq!(
1533+
urls[2].as_str(),
1534+
"https://auth.example.com/v1/mcp/.well-known/openid-configuration"
1535+
);
1536+
1537+
// Test URL with multiple path segments
1538+
let base_url = Url::parse("https://auth.example.com/tenant1/subtenant").unwrap();
1539+
let urls = AuthorizationManager::generate_discovery_urls(&base_url);
1540+
assert_eq!(urls.len(), 3);
1541+
assert_eq!(
1542+
urls[0].as_str(),
1543+
"https://auth.example.com/.well-known/oauth-authorization-server/tenant1/subtenant"
1544+
);
1545+
assert_eq!(
1546+
urls[1].as_str(),
1547+
"https://auth.example.com/.well-known/openid-configuration/tenant1/subtenant"
1548+
);
1549+
assert_eq!(
1550+
urls[2].as_str(),
1551+
"https://auth.example.com/tenant1/subtenant/.well-known/openid-configuration"
1552+
);
1553+
}
14631554
}

0 commit comments

Comments
 (0)