1use crate::{
2 auth::{OauthEndpoint, OAUTH_PROTECTED_RESOURCE_BASE, WELL_KNOWN_OAUTH_AUTHORIZATION_SERVER},
3 error::McpSdkError,
4 mcp_http::url_base,
5};
6use reqwest::Client;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use url::Url;
10
11#[derive(Debug, Serialize, Deserialize, Clone)]
12pub struct AuthorizationServerMetadata {
13 pub issuer: Url,
15
16 pub authorization_endpoint: Url,
18
19 pub token_endpoint: Url,
21
22 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
24 pub jwks_uri: Option<Url>,
25
26 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
28 pub registration_endpoint: Option<Url>,
29
30 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
32 pub scopes_supported: Option<Vec<String>>,
33
34 #[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")]
37 pub response_types_supported: Vec<String>,
38
39 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
42 pub response_modes_supported: Option<Vec<String>>,
43
44 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
50 pub grant_types_supported: Option<Vec<String>>,
51
52 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
54 pub token_endpoint_auth_methods_supported: Option<Vec<String>>,
55
56 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
58 pub token_endpoint_auth_signing_alg_values_supported: Option<Vec<String>>,
59
60 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
63 pub service_documentation: Option<Url>,
64
65 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
67 pub revocation_endpoint: Option<Url>,
68
69 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
71 pub revocation_endpoint_auth_signing_alg_values_supported: Option<Vec<String>>,
72
73 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
80 pub revocation_endpoint_auth_methods_supported: Option<Vec<String>>,
81
82 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
84 pub introspection_endpoint: Option<Url>,
85
86 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
88 pub introspection_endpoint_auth_methods_supported: Option<Vec<String>>,
89
90 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
92 pub introspection_endpoint_auth_signing_alg_values_supported: Option<Vec<String>>,
93
94 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
97 pub code_challenge_methods_supported: Option<Vec<String>>,
98
99 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
100 pub userinfo_endpoint: Option<String>,
101}
102
103impl AuthorizationServerMetadata {
104 pub fn new(
114 issuer: &str,
115 authorization_endpoint: &str,
116 token_endpoint: &str,
117 ) -> Result<Self, url::ParseError> {
118 let issuer = Url::parse(issuer)?;
119 let authorization_endpoint = Url::parse(authorization_endpoint)?;
120 let token_endpoint = Url::parse(token_endpoint)?;
121
122 Ok(Self {
123 issuer,
124 authorization_endpoint,
125 token_endpoint,
126 jwks_uri: Default::default(),
127 registration_endpoint: Default::default(),
128 scopes_supported: Default::default(),
129 response_types_supported: Default::default(),
130 response_modes_supported: Default::default(),
131 grant_types_supported: Default::default(),
132 token_endpoint_auth_methods_supported: Default::default(),
133 token_endpoint_auth_signing_alg_values_supported: Default::default(),
134 service_documentation: Default::default(),
135 revocation_endpoint: Default::default(),
136 revocation_endpoint_auth_signing_alg_values_supported: Default::default(),
137 revocation_endpoint_auth_methods_supported: Default::default(),
138 introspection_endpoint: Default::default(),
139 introspection_endpoint_auth_methods_supported: Default::default(),
140 introspection_endpoint_auth_signing_alg_values_supported: Default::default(),
141 code_challenge_methods_supported: Default::default(),
142 userinfo_endpoint: Default::default(),
143 })
144 }
145
146 pub async fn from_discovery_url(discovery_url: &str) -> Result<Self, McpSdkError> {
154 let client = Client::new();
155 let metadata = client
156 .get(discovery_url)
157 .send()
158 .await
159 .map_err(|err| McpSdkError::Internal {
160 description: err.to_string(),
161 })?
162 .json::<AuthorizationServerMetadata>()
163 .await
164 .map_err(|err| McpSdkError::Internal {
165 description: err.to_string(),
166 })?;
167 Ok(metadata)
168 }
169}
170
171#[derive(Debug, Serialize, Deserialize, Clone)]
176pub struct OauthProtectedResourceMetadata {
177 pub resource: Url,
180
181 #[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")]
184 pub authorization_servers: Vec<Url>,
185
186 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
190 pub jwks_uri: Option<Url>,
191
192 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
195 pub scopes_supported: Option<Vec<String>>,
196
197 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
202 pub bearer_methods_supported: Option<Vec<String>>,
203
204 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
207 pub resource_signing_alg_values_supported: Option<Vec<String>>,
208
209 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
212 pub resource_name: Option<String>,
213
214 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
216 pub resource_documentation: Option<String>,
217
218 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
220 pub resource_policy_uri: Option<Url>,
221
222 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
224 pub resource_tos_uri: Option<Url>,
225
226 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
229 pub tls_client_certificate_bound_access_tokens: Option<bool>,
230
231 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
234 pub authorization_details_types_supported: Option<Vec<String>>,
235
236 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
239 pub dpop_signing_alg_values_supported: Option<Vec<String>>,
240
241 #[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
244 pub dpop_bound_access_tokens_required: Option<bool>,
245}
246
247impl OauthProtectedResourceMetadata {
248 pub fn new<S>(
255 resource: S,
256 authorization_servers: Vec<S>,
257 scopes_supported: Option<Vec<String>>,
258 ) -> Result<Self, url::ParseError>
259 where
260 S: AsRef<str>,
261 {
262 let resource = Url::parse(resource.as_ref())?;
263 let authorization_servers: Vec<_> = authorization_servers
264 .iter()
265 .map(|s| Url::parse(s.as_ref()))
266 .collect::<Result<_, _>>()?;
267
268 Ok(Self {
269 resource,
270 authorization_servers,
271 jwks_uri: Default::default(),
272 scopes_supported,
273 bearer_methods_supported: Default::default(),
274 resource_signing_alg_values_supported: Default::default(),
275 resource_name: Default::default(),
276 resource_documentation: Default::default(),
277 resource_policy_uri: Default::default(),
278 resource_tos_uri: Default::default(),
279 tls_client_certificate_bound_access_tokens: Default::default(),
280 authorization_details_types_supported: Default::default(),
281 dpop_signing_alg_values_supported: Default::default(),
282 dpop_bound_access_tokens_required: Default::default(),
283 })
284 }
285}
286
287pub fn create_protected_resource_metadata_url(path: &str) -> String {
288 format!(
289 "{OAUTH_PROTECTED_RESOURCE_BASE}{}",
290 if path == "/" { "" } else { path }
291 )
292}
293
294pub fn create_discovery_endpoints(
295 mcp_server_url: &str,
296) -> Result<(HashMap<String, OauthEndpoint>, String), McpSdkError> {
297 let mut endpoint_map = HashMap::new();
298 endpoint_map.insert(
299 WELL_KNOWN_OAUTH_AUTHORIZATION_SERVER.to_string(),
300 OauthEndpoint::AuthorizationServerMetadata,
301 );
302
303 let resource_url = Url::parse(mcp_server_url).map_err(|err| McpSdkError::Internal {
304 description: err.to_string(),
305 })?;
306
307 let relative_url = create_protected_resource_metadata_url(resource_url.path());
308 let base_url = url_base(&resource_url);
309 let protected_resource_metadata_url =
310 format!("{}{relative_url}", base_url.trim_end_matches('/'));
311
312 endpoint_map.insert(relative_url, OauthEndpoint::ProtectedResourceMetadata);
313
314 Ok((endpoint_map, protected_resource_metadata_url))
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320 use serde_json::{json, Value};
321
322 fn sample_full_metadata_json() -> Value {
323 json!({
324 "issuer": "https://auth.example.com/realms/demo",
325 "authorization_endpoint": "https://auth.example.com/realms/demo/protocol/openid-connect/auth",
326 "token_endpoint": "https://auth.example.com/realms/demo/protocol/openid-connect/token",
327 "jwks_uri": "https://auth.example.com/realms/demo/protocol/openid-connect/certs",
328 "registration_endpoint": "https://auth.example.com/realms/demo/clients-registrations",
329 "scopes_supported": ["openid", "profile", "email", "mcp:tools", "offline_access"],
330 "response_types_supported": ["code", "id_token", "code id_token", "token"],
331 "response_modes_supported": ["query", "fragment", "form_post"],
332 "grant_types_supported": ["authorization_code", "refresh_token", "client_credentials"],
333 "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "private_key_jwt"],
334 "token_endpoint_auth_signing_alg_values_supported": ["RS256", "ES256"],
335 "service_documentation": "https://docs.example.com/oauth2",
336 "revocation_endpoint": "https://auth.example.com/realms/demo/protocol/openid-connect/revoke",
337 "revocation_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"],
338 "introspection_endpoint": "https://auth.example.com/realms/demo/protocol/openid-connect/token/introspect",
339 "code_challenge_methods_supported": ["S256", "plain"],
340 "userinfo_endpoint": "https://auth.example.com/realms/demo/protocol/openid-connect/userinfo"
341 })
342 }
343
344 #[test]
345 fn test_serialize_minimal_metadata() {
346 let meta = AuthorizationServerMetadata::new(
347 "https://auth.test/realms/min",
348 "https://auth.test/realms/min/auth",
349 "https://auth.test/realms/min/token",
350 )
351 .unwrap();
352
353 let json = serde_json::to_value(&meta).expect("serialize failed");
354
355 assert_eq!(json["issuer"], "https://auth.test/realms/min");
356 assert_eq!(
357 json["authorization_endpoint"],
358 "https://auth.test/realms/min/auth"
359 );
360 assert_eq!(json["token_endpoint"], "https://auth.test/realms/min/token");
361
362 assert!(!json.as_object().unwrap().contains_key("jwks_uri"));
364 assert!(!json.as_object().unwrap().contains_key("scopes_supported"));
365 assert_eq!(json["response_types_supported"], Value::Null);
366 }
367
368 #[test]
369 fn test_round_trip_minimal() {
370 let original = AuthorizationServerMetadata::new(
371 "https://issuer.example.com/",
372 "https://issuer.example.com/authorize",
373 "https://issuer.example.com/token",
374 )
375 .unwrap();
376
377 let json_str = serde_json::to_string(&original).unwrap();
378 let deserialized: AuthorizationServerMetadata = serde_json::from_str(&json_str).unwrap();
379
380 assert_eq!(original.issuer, deserialized.issuer);
381 assert_eq!(
382 original.authorization_endpoint,
383 deserialized.authorization_endpoint
384 );
385 assert_eq!(original.token_endpoint, deserialized.token_endpoint);
386 assert_eq!(original.jwks_uri, None);
387 assert_eq!(original.response_types_supported, Vec::<String>::new());
388 }
389
390 #[test]
391 fn test_deserialize_full_document() {
392 let json = sample_full_metadata_json();
393 let json_str = serde_json::to_string(&json).unwrap();
394
395 let meta: AuthorizationServerMetadata =
396 serde_json::from_str(&json_str).expect("deserialization failed");
397
398 assert_eq!(meta.issuer.as_str(), "https://auth.example.com/realms/demo");
399 assert_eq!(
400 meta.jwks_uri.as_ref().unwrap().as_str(),
401 "https://auth.example.com/realms/demo/protocol/openid-connect/certs"
402 );
403 assert_eq!(meta.scopes_supported.as_ref().unwrap().len(), 5);
404 assert!(meta
405 .scopes_supported
406 .as_ref()
407 .unwrap()
408 .contains(&"mcp:tools".to_string()));
409 assert_eq!(
410 meta.code_challenge_methods_supported.as_ref().unwrap(),
411 &vec!["S256".to_string(), "plain".to_string()]
412 );
413 assert_eq!(
414 meta.userinfo_endpoint.as_ref().unwrap(),
415 "https://auth.example.com/realms/demo/protocol/openid-connect/userinfo"
416 );
417 }
418
419 #[test]
420 fn test_round_trip_full_document() {
421 let json_val = sample_full_metadata_json();
422 let original: AuthorizationServerMetadata =
423 serde_json::from_value(json_val.clone()).unwrap();
424
425 let serialized = serde_json::to_value(&original).unwrap();
426 assert_eq!(serialized, json_val);
427
428 let json_str = serde_json::to_string(&original).unwrap();
430 let round_tripped: AuthorizationServerMetadata = serde_json::from_str(&json_str).unwrap();
431
432 assert_eq!(original.issuer, round_tripped.issuer);
433 assert_eq!(original.jwks_uri, round_tripped.jwks_uri);
434 assert_eq!(original.scopes_supported, round_tripped.scopes_supported);
435 assert_eq!(
436 original.response_types_supported,
437 round_tripped.response_types_supported
438 );
439 }
440
441 #[test]
442 fn test_deserialize_missing_required_field() {
443 let mut json = sample_full_metadata_json();
444 json.as_object_mut().unwrap().remove("token_endpoint");
445
446 let err = serde_json::from_value::<AuthorizationServerMetadata>(json).unwrap_err();
447 assert!(err.to_string().contains("token_endpoint"));
448 }
449
450 #[test]
451 fn test_deserialize_unknown_fields_are_ignored() {
452 let mut json = sample_full_metadata_json();
453 json["issuer"] = json!("https://auth.example.com/realms/demo");
454 json["some_new_field"] = json!(42);
455 json["claims_supported"] = json!(["sub", "name", "email"]); let meta: AuthorizationServerMetadata =
458 serde_json::from_value(json).expect("should ignore unknown fields");
459
460 assert_eq!(meta.issuer.as_str(), "https://auth.example.com/realms/demo");
461 }
462
463 #[test]
464 fn test_serialize_and_deserialize_with_empty_optional_arrays() {
465 let mut meta = AuthorizationServerMetadata::new(
466 "https://a.b/c",
467 "https://a.b/auth",
468 "https://a.b/token",
469 )
470 .unwrap();
471
472 meta.scopes_supported = Some(vec![]);
473 meta.grant_types_supported = Some(vec![]);
474 meta.response_modes_supported = None;
475
476 let json = serde_json::to_value(&meta).unwrap();
477
478 assert_eq!(json["scopes_supported"], Value::Array(vec![]));
480 assert_eq!(json["grant_types_supported"], Value::Array(vec![]));
481
482 assert!(!json
484 .as_object()
485 .unwrap()
486 .contains_key("response_modes_supported"));
487
488 let round: AuthorizationServerMetadata = serde_json::from_value(json).unwrap();
489 assert_eq!(round.scopes_supported, Some(vec![]));
490 assert_eq!(round.grant_types_supported, Some(vec![]));
491 assert_eq!(round.response_modes_supported, None);
492 let _ = serde_json::to_string(&round).unwrap();
493 }
494}