fix(oauth): support suffixed and preffixed well-knonw paths (#459)
Signed-off-by: Huabing Zhao <zhaohuabing@gmail.com>
This commit is contained in:
parent
717ec56e23
commit
0566d132ea
1 changed files with 121 additions and 38 deletions
|
|
@ -179,6 +179,33 @@ struct AuthorizationState {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AuthorizationManager {
|
impl AuthorizationManager {
|
||||||
|
fn well_known_paths(base_path: &str, resource: &str) -> Vec<String> {
|
||||||
|
let trimmed = base_path.trim_start_matches('/').trim_end_matches('/');
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
|
||||||
|
let mut push_candidate = |candidate: String| {
|
||||||
|
if !candidates.contains(&candidate) {
|
||||||
|
candidates.push(candidate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let canonical = format!("/.well-known/{resource}");
|
||||||
|
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
push_candidate(canonical);
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This follows the RFC 8414 recommendation for well-known URI discovery
|
||||||
|
push_candidate(format!("{canonical}/{trimmed}"));
|
||||||
|
// This is a common pattern used by some identity providers
|
||||||
|
push_candidate(format!("/{trimmed}/.well-known/{resource}"));
|
||||||
|
// The canonical path should always be the last fallback
|
||||||
|
push_candidate(canonical);
|
||||||
|
|
||||||
|
candidates
|
||||||
|
}
|
||||||
|
|
||||||
/// create new auth manager with base url
|
/// create new auth manager with base url
|
||||||
pub async fn new<U: IntoUrl>(base_url: U) -> Result<Self, AuthError> {
|
pub async fn new<U: IntoUrl>(base_url: U) -> Result<Self, AuthError> {
|
||||||
let base_url = base_url.into_url()?;
|
let base_url = base_url.into_url()?;
|
||||||
|
|
@ -207,53 +234,68 @@ impl AuthorizationManager {
|
||||||
|
|
||||||
/// discover oauth2 metadata
|
/// discover oauth2 metadata
|
||||||
pub async fn discover_metadata(&self) -> Result<AuthorizationMetadata, AuthError> {
|
pub async fn discover_metadata(&self) -> Result<AuthorizationMetadata, AuthError> {
|
||||||
// according to the specification, the metadata should be located at "/.well-known/oauth-authorization-server"
|
for candidate_path in
|
||||||
let mut discovery_url = self.base_url.clone();
|
Self::well_known_paths(self.base_url.path(), "oauth-authorization-server")
|
||||||
let path = discovery_url.path();
|
{
|
||||||
let path_suffix = if path == "/" { "" } else { path };
|
let mut discovery_url = self.base_url.clone();
|
||||||
discovery_url.set_path(&format!(
|
discovery_url.set_path(&candidate_path);
|
||||||
"/.well-known/oauth-authorization-server{path_suffix}"
|
debug!("discovery url: {:?}", discovery_url);
|
||||||
));
|
|
||||||
debug!("discovery url: {:?}", discovery_url);
|
|
||||||
let response = self
|
|
||||||
.http_client
|
|
||||||
.get(discovery_url)
|
|
||||||
.header("MCP-Protocol-Version", "2024-11-05")
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if response.status() == StatusCode::OK {
|
let response = match self
|
||||||
|
.http_client
|
||||||
|
.get(discovery_url)
|
||||||
|
.header("MCP-Protocol-Version", "2024-11-05")
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
debug!("discovery request failed: {}", e);
|
||||||
|
continue; // try next candidate if request fails
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if response.status() != StatusCode::OK {
|
||||||
|
debug!("discovery returned non-200: {}", response.status());
|
||||||
|
continue; // try next candidate if response is not OK
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse metadata
|
||||||
let metadata = response
|
let metadata = response
|
||||||
.json::<AuthorizationMetadata>()
|
.json::<AuthorizationMetadata>()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
|
// Fail the discovery if we get a 200 but cannot parse the response
|
||||||
|
// This indicates a misconfiguration on the server side
|
||||||
AuthError::MetadataError(format!("Failed to parse metadata: {}", e))
|
AuthError::MetadataError(format!("Failed to parse metadata: {}", e))
|
||||||
})?;
|
})?;
|
||||||
debug!("metadata: {:?}", metadata);
|
debug!("metadata: {:?}", metadata);
|
||||||
Ok(metadata)
|
return Ok(metadata);
|
||||||
} else {
|
|
||||||
// fallback to default endpoints
|
|
||||||
let mut auth_base = self.base_url.clone();
|
|
||||||
// discard the path part, only keep scheme, host, port
|
|
||||||
auth_base.set_path("");
|
|
||||||
|
|
||||||
// Helper function to create endpoint URL
|
|
||||||
let create_endpoint = |path: &str| -> String {
|
|
||||||
let mut url = auth_base.clone();
|
|
||||||
url.set_path(path);
|
|
||||||
url.to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(AuthorizationMetadata {
|
|
||||||
authorization_endpoint: create_endpoint("authorize"),
|
|
||||||
token_endpoint: create_endpoint("token"),
|
|
||||||
registration_endpoint: create_endpoint("register"),
|
|
||||||
issuer: None,
|
|
||||||
jwks_uri: None,
|
|
||||||
scopes_supported: None,
|
|
||||||
additional_fields: HashMap::new(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debug!("No valid .well-known endpoint found, falling back to default endpoints");
|
||||||
|
|
||||||
|
// fallback to default endpoints
|
||||||
|
let mut auth_base = self.base_url.clone();
|
||||||
|
// discard the path part, only keep scheme, host, port
|
||||||
|
auth_base.set_path("");
|
||||||
|
|
||||||
|
// Helper function to create endpoint URL
|
||||||
|
let create_endpoint = |path: &str| -> String {
|
||||||
|
let mut url = auth_base.clone();
|
||||||
|
url.set_path(path);
|
||||||
|
url.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(AuthorizationMetadata {
|
||||||
|
authorization_endpoint: create_endpoint("authorize"),
|
||||||
|
token_endpoint: create_endpoint("token"),
|
||||||
|
registration_endpoint: create_endpoint("register"),
|
||||||
|
issuer: None,
|
||||||
|
jwks_uri: None,
|
||||||
|
scopes_supported: None,
|
||||||
|
additional_fields: HashMap::new(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// get client id and credentials
|
/// get client id and credentials
|
||||||
|
|
@ -876,3 +918,44 @@ impl OAuthState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::AuthorizationManager;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn well_known_paths_root() {
|
||||||
|
let paths = AuthorizationManager::well_known_paths("/", "oauth-authorization-server");
|
||||||
|
assert_eq!(
|
||||||
|
paths,
|
||||||
|
vec!["/.well-known/oauth-authorization-server".to_string()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn well_known_paths_with_suffix() {
|
||||||
|
let paths = AuthorizationManager::well_known_paths("/mcp", "oauth-authorization-server");
|
||||||
|
assert_eq!(
|
||||||
|
paths,
|
||||||
|
vec![
|
||||||
|
"/.well-known/oauth-authorization-server/mcp".to_string(),
|
||||||
|
"/mcp/.well-known/oauth-authorization-server".to_string(),
|
||||||
|
"/.well-known/oauth-authorization-server".to_string(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn well_known_paths_trailing_slash() {
|
||||||
|
let paths =
|
||||||
|
AuthorizationManager::well_known_paths("/v1/mcp/", "oauth-authorization-server");
|
||||||
|
assert_eq!(
|
||||||
|
paths,
|
||||||
|
vec![
|
||||||
|
"/.well-known/oauth-authorization-server/v1/mcp".to_string(),
|
||||||
|
"/v1/mcp/.well-known/oauth-authorization-server".to_string(),
|
||||||
|
"/.well-known/oauth-authorization-server".to_string(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue