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