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 <tanishdesai37@gmail.com> * fix: format auth.rs test assertions Reformat assert_eq! statements to satisfy rustfmt checks in CI. Signed-off-by: tanish111 <tanishdesai37@gmail.com> --------- Signed-off-by: tanish111 <tanishdesai37@gmail.com>
This commit is contained in:
parent
63d89b1a4e
commit
61f7b7b99e
1 changed files with 97 additions and 6 deletions
|
|
@ -687,16 +687,40 @@ impl AuthorizationManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate discovery endpoint URLs following the priority order in spec-2025-11-25 4.3 "Authorization Server Metadata Discovery".
|
||||||
|
fn generate_discovery_urls(base_url: &Url) -> Vec<Url> {
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
let path = base_url.path();
|
||||||
|
let trimmed = path.trim_start_matches('/').trim_end_matches('/');
|
||||||
|
let mut push_candidate = |discovery_path: String| {
|
||||||
|
let mut discovery_url = base_url.clone();
|
||||||
|
discovery_url.set_query(None);
|
||||||
|
discovery_url.set_fragment(None);
|
||||||
|
discovery_url.set_path(&discovery_path);
|
||||||
|
candidates.push(discovery_url);
|
||||||
|
};
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
// No path components: try OAuth first, then OpenID Connect
|
||||||
|
push_candidate("/.well-known/oauth-authorization-server".to_string());
|
||||||
|
push_candidate("/.well-known/openid-configuration".to_string());
|
||||||
|
} else {
|
||||||
|
// Path components present: follow spec priority order
|
||||||
|
// 1. OAuth 2.0 with path insertion
|
||||||
|
push_candidate(format!("/.well-known/oauth-authorization-server/{trimmed}"));
|
||||||
|
// 2. OpenID Connect with path insertion
|
||||||
|
push_candidate(format!("/.well-known/openid-configuration/{trimmed}"));
|
||||||
|
// 3. OpenID Connect with path appending
|
||||||
|
push_candidate(format!("/{trimmed}/.well-known/openid-configuration"));
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates
|
||||||
|
}
|
||||||
|
|
||||||
async fn try_discover_oauth_server(
|
async fn try_discover_oauth_server(
|
||||||
&self,
|
&self,
|
||||||
base_url: &Url,
|
base_url: &Url,
|
||||||
) -> Result<Option<AuthorizationMetadata>, AuthError> {
|
) -> Result<Option<AuthorizationMetadata>, AuthError> {
|
||||||
for candidate_path in Self::well_known_paths(base_url.path(), "oauth-authorization-server")
|
for discovery_url in Self::generate_discovery_urls(base_url) {
|
||||||
{
|
|
||||||
let mut discovery_url = base_url.clone();
|
|
||||||
discovery_url.set_query(None);
|
|
||||||
discovery_url.set_fragment(None);
|
|
||||||
discovery_url.set_path(&candidate_path);
|
|
||||||
if let Some(metadata) = self.fetch_authorization_metadata(&discovery_url).await? {
|
if let Some(metadata) = self.fetch_authorization_metadata(&discovery_url).await? {
|
||||||
return Ok(Some(metadata));
|
return Ok(Some(metadata));
|
||||||
}
|
}
|
||||||
|
|
@ -1460,4 +1484,71 @@ mod tests {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn generate_discovery_urls() {
|
||||||
|
// Test root URL (no path components): OAuth first, then OpenID Connect
|
||||||
|
let base_url = Url::parse("https://auth.example.com").unwrap();
|
||||||
|
let urls = AuthorizationManager::generate_discovery_urls(&base_url);
|
||||||
|
assert_eq!(urls.len(), 2);
|
||||||
|
assert_eq!(
|
||||||
|
urls[0].as_str(),
|
||||||
|
"https://auth.example.com/.well-known/oauth-authorization-server"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
urls[1].as_str(),
|
||||||
|
"https://auth.example.com/.well-known/openid-configuration"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test URL with single path segment: follow spec priority order
|
||||||
|
let base_url = Url::parse("https://auth.example.com/tenant1").unwrap();
|
||||||
|
let urls = AuthorizationManager::generate_discovery_urls(&base_url);
|
||||||
|
assert_eq!(urls.len(), 3);
|
||||||
|
assert_eq!(
|
||||||
|
urls[0].as_str(),
|
||||||
|
"https://auth.example.com/.well-known/oauth-authorization-server/tenant1"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
urls[1].as_str(),
|
||||||
|
"https://auth.example.com/.well-known/openid-configuration/tenant1"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
urls[2].as_str(),
|
||||||
|
"https://auth.example.com/tenant1/.well-known/openid-configuration"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test URL with path and trailing slash
|
||||||
|
let base_url = Url::parse("https://auth.example.com/v1/mcp/").unwrap();
|
||||||
|
let urls = AuthorizationManager::generate_discovery_urls(&base_url);
|
||||||
|
assert_eq!(urls.len(), 3);
|
||||||
|
assert_eq!(
|
||||||
|
urls[0].as_str(),
|
||||||
|
"https://auth.example.com/.well-known/oauth-authorization-server/v1/mcp"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
urls[1].as_str(),
|
||||||
|
"https://auth.example.com/.well-known/openid-configuration/v1/mcp"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
urls[2].as_str(),
|
||||||
|
"https://auth.example.com/v1/mcp/.well-known/openid-configuration"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test URL with multiple path segments
|
||||||
|
let base_url = Url::parse("https://auth.example.com/tenant1/subtenant").unwrap();
|
||||||
|
let urls = AuthorizationManager::generate_discovery_urls(&base_url);
|
||||||
|
assert_eq!(urls.len(), 3);
|
||||||
|
assert_eq!(
|
||||||
|
urls[0].as_str(),
|
||||||
|
"https://auth.example.com/.well-known/oauth-authorization-server/tenant1/subtenant"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
urls[1].as_str(),
|
||||||
|
"https://auth.example.com/.well-known/openid-configuration/tenant1/subtenant"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
urls[2].as_str(),
|
||||||
|
"https://auth.example.com/tenant1/subtenant/.well-known/openid-configuration"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue