-
-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathoauth_utils.rs
More file actions
149 lines (133 loc) · 5.34 KB
/
Copy pathoauth_utils.rs
File metadata and controls
149 lines (133 loc) · 5.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
//! Shared OAuth utilities for metadata discovery with origin URL fallback.
//!
//! Some OAuth servers (like Atlassian) serve their metadata at the origin URL
//! (e.g., `https://mcp.atlassian.com`) rather than the endpoint path
//! (e.g., `https://mcp.atlassian.com/v1/sse`). This module provides utilities
//! to handle both cases.
use mcpmux_core::StoredOAuthMetadata;
use rmcp::transport::auth::{AuthError, AuthorizationManager, AuthorizationMetadata};
use tracing::info;
use url::Url;
/// Extract the origin (scheme + host + port) from a URL.
///
/// # Example
/// ```ignore
/// extract_origin("https://mcp.atlassian.com/v1/sse") // -> Some("https://mcp.atlassian.com")
/// extract_origin("http://localhost:8080/api") // -> Some("http://localhost:8080")
/// ```
pub fn extract_origin(url: &str) -> Option<String> {
let parsed = Url::parse(url).ok()?;
let host = parsed.host_str()?;
let mut origin = format!("{}://{}", parsed.scheme(), host);
if let Some(port) = parsed.port() {
origin = format!("{}:{}", origin, port);
}
Some(origin)
}
/// Discover OAuth metadata with fallback to origin URL.
///
/// This tries to discover metadata at the server URL first. If that fails with
/// `NoAuthorizationSupport`, it extracts the origin and tries there.
///
/// Returns the discovered metadata if successful, or an error if both attempts fail.
pub async fn discover_metadata_with_fallback(
manager: &mut AuthorizationManager,
server_url: &str,
) -> Result<AuthorizationMetadata, AuthError> {
// First try the direct URL
match manager.discover_metadata().await {
Ok(metadata) => {
info!("[OAuth] Metadata discovered at endpoint: {}", server_url);
Ok(metadata)
}
Err(AuthError::NoAuthorizationSupport) => {
// Try origin URL as fallback
let origin_url = extract_origin(server_url).ok_or(AuthError::NoAuthorizationSupport)?;
info!(
"[OAuth] Metadata not at endpoint, trying origin: {}",
origin_url
);
let origin_manager = AuthorizationManager::new(&origin_url)
.await
.map_err(|_| AuthError::NoAuthorizationSupport)?;
let metadata = origin_manager.discover_metadata().await?;
info!("[OAuth] Metadata discovered at origin: {}", origin_url);
Ok(metadata)
}
Err(e) => Err(e),
}
}
/// Discover metadata and return both the RMCP metadata (for setting on manager)
/// and our stored format (for persistence).
///
/// Use this when you need to both configure RMCP and save metadata for future reconnects.
pub async fn discover_and_convert_metadata(
manager: &mut AuthorizationManager,
server_url: &str,
) -> Result<(AuthorizationMetadata, StoredOAuthMetadata), AuthError> {
let metadata = discover_metadata_with_fallback(manager, server_url).await?;
let stored = convert_to_stored_metadata(&metadata);
Ok((metadata, stored))
}
/// Convert RMCP's AuthorizationMetadata to our StoredOAuthMetadata format.
///
/// This allows us to persist discovered metadata and later use it to bypass
/// RMCP's metadata discovery (which can fail on non-spec-compliant servers).
pub fn convert_to_stored_metadata(metadata: &AuthorizationMetadata) -> StoredOAuthMetadata {
StoredOAuthMetadata {
authorization_endpoint: metadata.authorization_endpoint.clone(),
token_endpoint: metadata.token_endpoint.clone(),
registration_endpoint: metadata.registration_endpoint.clone(),
issuer: metadata.issuer.clone(),
jwks_uri: metadata.jwks_uri.clone(),
scopes_supported: metadata.scopes_supported.clone(),
response_types_supported: metadata.response_types_supported.clone(),
additional_fields: metadata.additional_fields.clone(),
}
}
/// Convert our StoredOAuthMetadata back to RMCP's AuthorizationMetadata format.
///
/// This is used when loading saved metadata and setting it on the RMCP manager
/// to bypass discovery.
pub fn convert_from_stored_metadata(stored: &StoredOAuthMetadata) -> AuthorizationMetadata {
AuthorizationMetadata {
authorization_endpoint: stored.authorization_endpoint.clone(),
token_endpoint: stored.token_endpoint.clone(),
registration_endpoint: stored.registration_endpoint.clone(),
issuer: stored.issuer.clone(),
jwks_uri: stored.jwks_uri.clone(),
scopes_supported: stored.scopes_supported.clone(),
response_types_supported: stored.response_types_supported.clone(),
additional_fields: stored.additional_fields.clone(),
..Default::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_origin_with_path() {
assert_eq!(
extract_origin("https://mcp.atlassian.com/v1/sse"),
Some("https://mcp.atlassian.com".to_string())
);
}
#[test]
fn test_extract_origin_with_port() {
assert_eq!(
extract_origin("http://localhost:8080/api/v1"),
Some("http://localhost:8080".to_string())
);
}
#[test]
fn test_extract_origin_no_path() {
assert_eq!(
extract_origin("https://example.com"),
Some("https://example.com".to_string())
);
}
#[test]
fn test_extract_origin_invalid_url() {
assert_eq!(extract_origin("not a url"), None);
}
}