Add SEP-991 (CIMD) support for URL-based client IDs (#570)
* feat(auth): add cimd support for SEP-991 add cimd support for url-based client ids Signed-off-by: tanish111 <tanishdesai37@gmail.com> * test(auth): add unit tests for is_https_url helper Add test coverage for is_https_url helper to validate HTTPS scheme, non-root paths, and reject http, javascript, data schemes, and invalid inputs per SEP-991 requirements. Signed-off-by: tanish111 <tanishdesai37@gmail.com> * feat(example): add CIMD OAuth server for SEP-991 testing Implements a new server example (servers_cimd_auth_streamhttp) that demonstrates CIMD (Client ID Metadata Document) support for URL-based client IDs. The server validates client_id URLs, fetches and validates client metadata documents, and provides OAuth 2.0 authorization endpoints with MCP integration for end-to-end testing. Signed-off-by: tanish111 <tanishdesai37@gmail.com> * fix(oauth): add CORS headers to token endpoint Add CORS headers to token endpoint to allow cross-origin requests from browsers during OAuth authorization code exchange flow. Signed-off-by: tanish111 <tanishdesai37@gmail.com> * refactor: improve is_https_url function and consolidate tests - Improve is_https_url function formatting and readability - Merge all test cases into single test_is_https_url_scenarios function - Add missing test case for "https://" URL Signed-off-by: tanish111 <tanishdesai37@gmail.com> * refactor: use map_err instead of match for error handling in auth.rs Replace the verbose match statement with map_err for more idiomatic Signed-off-by: tanish111 <tanishdesai37@gmail.com> * feat: add client-metadata.json Add client metadata file for SEP-991 CIMD authentication support Signed-off-by: tanish111 <tanishdesai37@gmail.com> --------- Signed-off-by: tanish111 <tanishdesai37@gmail.com>
This commit is contained in:
parent
e3fd384a45
commit
f20ed202af
1 changed files with 656 additions and 30 deletions
|
|
@ -240,6 +240,15 @@ struct AuthorizationState {
|
|||
csrf_token: CsrfToken,
|
||||
}
|
||||
|
||||
/// SEP-991: URL-based Client IDs
|
||||
/// Validate that the client_id is a valid URL with https scheme and non-root pathname
|
||||
fn is_https_url(value: &str) -> bool {
|
||||
Url::parse(value)
|
||||
.ok()
|
||||
.map(|url| url.scheme() == "https" && url.path() != "/" && url.host_str().is_some())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
impl AuthorizationManager {
|
||||
fn well_known_paths(base_path: &str, resource: &str) -> Vec<String> {
|
||||
let trimmed = base_path.trim_start_matches('/').trim_end_matches('/');
|
||||
|
|
@ -968,30 +977,57 @@ impl AuthorizationSession {
|
|||
scopes: &[&str],
|
||||
redirect_uri: &str,
|
||||
client_name: Option<&str>,
|
||||
client_metadata_url: Option<&str>,
|
||||
) -> Result<Self, AuthError> {
|
||||
// Default client config
|
||||
let config = OAuthClientConfig {
|
||||
client_id: "mcp-client".to_string(),
|
||||
client_secret: None,
|
||||
scopes: scopes.iter().map(|s| s.to_string()).collect(),
|
||||
redirect_uri: redirect_uri.to_string(),
|
||||
};
|
||||
let metadata = auth_manager.metadata.as_ref();
|
||||
let supports_url_based_client_id = metadata
|
||||
.and_then(|m| {
|
||||
m.additional_fields
|
||||
.get("client_id_metadata_document_supported")
|
||||
})
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false);
|
||||
|
||||
// try to dynamic register client
|
||||
let config = match auth_manager
|
||||
.register_client(client_name.unwrap_or("MCP Client"), redirect_uri)
|
||||
.await
|
||||
{
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Dynamic registration failed: {}, fallback to default config",
|
||||
e
|
||||
);
|
||||
// fallback to default config
|
||||
config
|
||||
let config = if supports_url_based_client_id {
|
||||
if let Some(client_metadata_url) = client_metadata_url {
|
||||
if !is_https_url(client_metadata_url) {
|
||||
return Err(AuthError::RegistrationFailed(format!(
|
||||
"client_metadata_url must be a valid HTTPS URL with a non-root pathname, got: {}",
|
||||
client_metadata_url
|
||||
)));
|
||||
}
|
||||
// SEP-991: URL-based Client IDs - use URL as client_id directly
|
||||
OAuthClientConfig {
|
||||
client_id: client_metadata_url.to_string(),
|
||||
client_secret: None,
|
||||
scopes: scopes.iter().map(|s| s.to_string()).collect(),
|
||||
redirect_uri: redirect_uri.to_string(),
|
||||
}
|
||||
} else {
|
||||
// Fallback to dynamic registration
|
||||
auth_manager
|
||||
.register_client(client_name.unwrap_or("MCP Client"), redirect_uri)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
AuthError::RegistrationFailed(format!("Dynamic registration failed: {}", e))
|
||||
})?
|
||||
}
|
||||
} else {
|
||||
// Fallback to dynamic registration
|
||||
match auth_manager
|
||||
.register_client(client_name.unwrap_or("MCP Client"), redirect_uri)
|
||||
.await
|
||||
{
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
return Err(AuthError::RegistrationFailed(format!(
|
||||
"Dynamic registration failed: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// reset client config
|
||||
auth_manager.configure_client(config)?;
|
||||
let auth_url = auth_manager.get_authorization_url(scopes).await?;
|
||||
|
|
@ -1143,6 +1179,18 @@ impl OAuthState {
|
|||
scopes: &[&str],
|
||||
redirect_uri: &str,
|
||||
client_name: Option<&str>,
|
||||
) -> Result<(), AuthError> {
|
||||
self.start_authorization_with_metadata_url(scopes, redirect_uri, client_name, None)
|
||||
.await
|
||||
}
|
||||
|
||||
/// start authorization with optional client metadata URL (SEP-991)
|
||||
pub async fn start_authorization_with_metadata_url(
|
||||
&mut self,
|
||||
scopes: &[&str],
|
||||
redirect_uri: &str,
|
||||
client_name: Option<&str>,
|
||||
client_metadata_url: Option<&str>,
|
||||
) -> Result<(), AuthError> {
|
||||
if let OAuthState::Unauthorized(mut manager) = std::mem::replace(
|
||||
self,
|
||||
|
|
@ -1152,8 +1200,14 @@ impl OAuthState {
|
|||
let metadata = manager.discover_metadata().await?;
|
||||
manager.metadata = Some(metadata);
|
||||
debug!("start session");
|
||||
let session =
|
||||
AuthorizationSession::new(manager, scopes, redirect_uri, client_name).await?;
|
||||
let session = AuthorizationSession::new(
|
||||
manager,
|
||||
scopes,
|
||||
redirect_uri,
|
||||
client_name,
|
||||
client_metadata_url,
|
||||
)
|
||||
.await?;
|
||||
*self = OAuthState::Session(session);
|
||||
Ok(())
|
||||
} else {
|
||||
|
|
@ -1274,7 +1328,31 @@ impl OAuthState {
|
|||
mod tests {
|
||||
use url::Url;
|
||||
|
||||
use super::AuthorizationManager;
|
||||
use super::{AuthorizationManager, is_https_url};
|
||||
|
||||
// SEP-991: URL-based Client IDs
|
||||
// Tests adapted from the TypeScript SDK's isHttpsUrl test suite
|
||||
#[test]
|
||||
fn test_is_https_url_scenarios() {
|
||||
// Returns true for valid https url with path
|
||||
assert!(is_https_url("https://example.com/client-metadata.json"));
|
||||
// Returns true for https url with query params
|
||||
assert!(is_https_url("https://example.com/metadata?version=1"));
|
||||
// Returns false for https url without path
|
||||
assert!(!is_https_url("https://example.com"));
|
||||
assert!(!is_https_url("https://example.com/"));
|
||||
assert!(!is_https_url("https://"));
|
||||
// Returns false for http url
|
||||
assert!(!is_https_url("http://example.com/metadata"));
|
||||
// Returns false for non-url strings
|
||||
assert!(!is_https_url("not a url"));
|
||||
// Returns false for empty string
|
||||
assert!(!is_https_url(""));
|
||||
// Returns false for javascript scheme
|
||||
assert!(!is_https_url("javascript:alert(1)"));
|
||||
// Returns false for data scheme
|
||||
assert!(!is_https_url("data:text/html,<script>alert(1)</script>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_resource_metadata_parameter() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue