1use std::borrow::Cow;
2
3use crate::{
4 auth::{AuthorizationServerMetadata, OauthProtectedResourceMetadata},
5 error::McpSdkError,
6 utils::join_url,
7};
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use thiserror::Error;
11use url::Url;
12
13pub const WELL_KNOWN_OAUTH_AUTHORIZATION_SERVER: &str = "/.well-known/oauth-authorization-server";
14pub const OAUTH_PROTECTED_RESOURCE_BASE: &str = "/.well-known/oauth-protected-resource";
15
16#[allow(unused)]
17#[derive(Hash, Eq, PartialEq, Clone)]
18pub enum OauthEndpoint {
19 AuthorizationEndpoint,
20 TokenEndpoint,
21 RegistrationEndpoint,
22 RevocationEndpoint,
23 IntrospectionEndpoint,
24 AuthorizationServerMetadata,
25 ProtectedResourceMetadata,
26}
27
28#[derive(Debug, Error)]
29pub enum AuthMetadateError {
30 #[error("Url Parse Error: {0}")]
31 Transport(#[from] url::ParseError),
32}
33
34pub struct AuthMetadataEndpoints {
35 pub protected_resource_endpoint: String,
36 pub authorization_server_endpoint: String,
37}
38
39#[derive(Default)]
42pub struct AuthMetadataBuilder<'a> {
43 issuer: Option<Cow<'a, str>>,
45 authorization_endpoint: Option<Cow<'a, str>>,
46 token_endpoint: Option<Cow<'a, str>>,
47 registration_endpoint: Option<Cow<'a, str>>,
48 revocation_endpoint: Option<Cow<'a, str>>,
49 introspection_endpoint: Option<Cow<'a, str>>,
50 scopes_supported: Option<Vec<Cow<'a, str>>>,
51
52 response_types_supported: Option<Vec<Cow<'a, str>>>,
53 response_modes_supported: Option<Vec<Cow<'a, str>>>,
54 grant_types_supported: Option<Vec<Cow<'a, str>>>,
55 token_endpoint_auth_methods_supported: Option<Vec<Cow<'a, str>>>,
56 token_endpoint_auth_signing_alg_values_supported: Option<Vec<Cow<'a, str>>>,
57 revocation_endpoint_auth_signing_alg_values_supported: Option<Vec<Cow<'a, str>>>,
58 revocation_endpoint_auth_methods_supported: Option<Vec<Cow<'a, str>>>,
59 introspection_endpoint_auth_methods_supported: Option<Vec<Cow<'a, str>>>,
60 introspection_endpoint_auth_signing_alg_values_supported: Option<Vec<Cow<'a, str>>>,
61 code_challenge_methods_supported: Option<Vec<Cow<'a, str>>>,
62 service_documentation: Option<Cow<'a, str>>,
63
64 resource: Option<Cow<'a, str>>,
66 authorization_servers: Option<Vec<Cow<'a, str>>>,
67 required_scopes: Option<Vec<Cow<'a, str>>>,
68
69 jwks_uri: Option<Cow<'a, str>>,
70 bearer_methods_supported: Option<Vec<Cow<'a, str>>>,
71 resource_signing_alg_values_supported: Option<Vec<Cow<'a, str>>>,
72 resource_name: Option<Cow<'a, str>>,
73 resource_documentation: Option<Cow<'a, str>>,
74 resource_policy_uri: Option<Cow<'a, str>>,
75 resource_tos_uri: Option<Cow<'a, str>>,
76 tls_client_certificate_bound_access_tokens: Option<bool>,
77 authorization_details_types_supported: Option<Vec<Cow<'a, str>>>,
78 dpop_signing_alg_values_supported: Option<Vec<Cow<'a, str>>>,
79 dpop_bound_access_tokens_required: Option<bool>,
80
81 userinfo_endpoint: Option<Cow<'a, str>>,
83}
84
85#[derive(Debug, Serialize, Deserialize, Clone)]
87pub struct OauthMetadata {
88 authorization_server_metadata: AuthorizationServerMetadata,
89 protected_resource_metadata: OauthProtectedResourceMetadata,
90}
91
92impl OauthMetadata {
93 pub fn protected_resource_metadata(&self) -> &OauthProtectedResourceMetadata {
94 &self.protected_resource_metadata
95 }
96
97 pub fn authorization_server_metadata(&self) -> &AuthorizationServerMetadata {
98 &self.authorization_server_metadata
99 }
100
101 pub fn endpoints(&self) -> AuthMetadataEndpoints {
102 AuthMetadataEndpoints {
103 authorization_server_endpoint: WELL_KNOWN_OAUTH_AUTHORIZATION_SERVER.to_string(),
104 protected_resource_endpoint: format!(
105 "{OAUTH_PROTECTED_RESOURCE_BASE}{}",
106 match self.protected_resource_metadata.resource.path() {
107 "/" => "",
108 other => other,
109 }
110 ),
111 }
112 }
113}
114
115impl<'a> AuthMetadataBuilder<'a> {
116 fn with_defaults(protected_resource: &'a str) -> Self {
117 Self {
118 response_types_supported: Some(vec!["code".into()]),
119 code_challenge_methods_supported: Some(vec!["S256".into()]),
120 token_endpoint_auth_methods_supported: Some(vec!["client_secret_post".into()]),
121 grant_types_supported: Some(vec!["authorization_code".into(), "refresh_token".into()]),
122 resource: Some(protected_resource.into()),
123 ..Default::default()
124 }
125 }
126
127 pub fn new(protected_resource_url: &'a str) -> Self {
130 Self::with_defaults(protected_resource_url)
131 }
132
133 pub async fn from_discovery_url<S>(
134 discovery_url: &str,
135 protected_resource: S,
136 required_scopes: Vec<S>,
137 ) -> Result<Self, McpSdkError>
138 where
139 S: Into<Cow<'a, str>>,
140 {
141 let client = crate::auth::shared_http_client();
142 let json: Value = client
143 .get(discovery_url)
144 .send()
145 .await
146 .map_err(|e| McpSdkError::Internal {
147 description: format!(
148 "Failed to fetch discovery document : \"{discovery_url}\": {e}"
149 ),
150 })?
151 .error_for_status()
152 .map_err(|e| McpSdkError::Internal {
153 description: format!("Discovery endpoint returned error: {e}"),
154 })?
155 .json()
156 .await
157 .map_err(|e| McpSdkError::Internal {
158 description: format!("Failed to parse JSON from discovery document: {e}"),
159 })?;
160
161 let get_str = |key: &str| {
163 json.get(key)
164 .and_then(|v| v.as_str())
165 .map(|s| Cow::<str>::Owned(s.to_string()))
166 };
167 let get_str_array = |key: &str| {
169 json.get(key).and_then(|v| v.as_array()).map(|arr| {
170 arr.iter()
171 .filter_map(|item| item.as_str())
172 .filter(|v| !v.is_empty())
173 .map(|s| Cow::<str>::Owned(s.to_string()))
174 .collect::<Vec<_>>()
175 })
176 };
177
178 let issuer = get_str("issuer").ok_or_else(|| McpSdkError::Internal {
179 description: "Missing 'issuer' in discovery document".to_string(),
180 })?;
181
182 Ok(Self {
183 issuer: Some(issuer.clone()),
184 authorization_endpoint: get_str("authorization_endpoint"),
185 scopes_supported: get_str_array("scopes_supported"),
186 required_scopes: Some(required_scopes.into_iter().map(|s| s.into()).collect()),
187 token_endpoint: get_str("token_endpoint"),
188 jwks_uri: get_str("jwks_uri"),
189
190 userinfo_endpoint: get_str("userinfo_endpoint"),
191
192 registration_endpoint: get_str("registration_endpoint"),
193 revocation_endpoint: get_str("revocation_endpoint"),
194 introspection_endpoint: get_str("introspection_endpoint"),
195 response_types_supported: get_str_array("response_types_supported"),
196 response_modes_supported: get_str_array("response_modes_supported"),
197 grant_types_supported: get_str_array("grant_types_supported"),
198 token_endpoint_auth_methods_supported: get_str_array(
199 "token_endpoint_auth_methods_supported",
200 ),
201 token_endpoint_auth_signing_alg_values_supported: get_str_array(
202 "token_endpoint_auth_signing_alg_values_supported",
203 ),
204 revocation_endpoint_auth_signing_alg_values_supported: get_str_array(
205 "revocation_endpoint_auth_signing_alg_values_supported",
206 ),
207 revocation_endpoint_auth_methods_supported: get_str_array(
208 "revocation_endpoint_auth_methods_supported",
209 ),
210 introspection_endpoint_auth_methods_supported: get_str_array(
211 "introspection_endpoint_auth_methods_supported",
212 ),
213 introspection_endpoint_auth_signing_alg_values_supported: get_str_array(
214 "introspection_endpoint_auth_signing_alg_values_supported",
215 ),
216 code_challenge_methods_supported: get_str_array("code_challenge_methods_supported"),
217 service_documentation: get_str("service_documentation"),
218 resource: Some(protected_resource.into()),
219 authorization_servers: Some(vec![issuer]),
220 bearer_methods_supported: None,
221 resource_signing_alg_values_supported: None,
222 resource_name: None,
223 resource_documentation: None,
224 resource_policy_uri: None,
225 resource_tos_uri: None,
226 tls_client_certificate_bound_access_tokens: None,
227 authorization_details_types_supported: None,
228 dpop_signing_alg_values_supported: None,
229 dpop_bound_access_tokens_required: None,
230 })
231 }
232
233 fn parse_url_field<S>(
234 field_name: &str,
235 value: Option<S>,
236 base_url: Option<&Url>,
237 ) -> Result<Url, McpSdkError>
238 where
239 S: Into<Cow<'a, str>>,
240 {
241 let value = value
242 .ok_or(McpSdkError::Internal {
243 description: format!("Error: '{field_name}' is missing."),
244 })?
245 .into();
246
247 let url = if value.contains("://") {
248 Url::parse(&value)
250 } else if let Some(base_url) = base_url {
251 join_url(base_url, &value)
253 } else {
254 Url::parse(&value)
256 };
257
258 url.map_err(|e| McpSdkError::Internal {
259 description: format!("Error: '{field_name}' is not a valid URL: {e}"),
260 })
261 }
262
263 fn parse_optional_url_field<S>(
264 field_name: &str,
265 value: Option<S>,
266 base_url: Option<&Url>,
267 ) -> Result<Option<Url>, McpSdkError>
268 where
269 S: Into<Cow<'a, str>>,
270 {
271 value
272 .map(|v| {
273 let value = v.into();
274 if value.contains("://") {
275 Url::parse(&value)
277 } else if let Some(base_url) = base_url {
278 join_url(base_url, &value)
280 } else {
281 Url::parse(&value)
283 }
284 })
285 .transpose()
286 .map_err(|e| McpSdkError::Internal {
287 description: format!("Error: '{field_name}' is not a valid URL: {e}"),
288 })
289 }
290
291 pub fn scopes_supported<S>(mut self, scopes: Vec<S>) -> Self
292 where
293 S: Into<Cow<'a, str>>,
294 {
295 self.scopes_supported = Some(scopes.into_iter().map(|s| s.into()).collect());
296 self
297 }
298
299 pub fn issuer<S>(mut self, issuer: S) -> Self
301 where
302 S: Into<Cow<'a, str>>,
303 {
304 self.issuer = Some(issuer.into());
305 self
306 }
307
308 pub fn service_documentation<S>(mut self, url: S) -> Self
309 where
310 S: Into<Cow<'a, str>>,
311 {
312 self.service_documentation = Some(url.into());
313 self
314 }
315
316 pub fn authorization_endpoint<S>(mut self, url: S) -> Self
317 where
318 S: Into<Cow<'a, str>>,
319 {
320 self.authorization_endpoint = Some(url.into());
321 self
322 }
323
324 pub fn token_endpoint<S>(mut self, url: S) -> Self
325 where
326 S: Into<Cow<'a, str>>,
327 {
328 self.token_endpoint = Some(url.into());
329 self
330 }
331
332 pub fn response_types_supported<S>(mut self, types: Vec<S>) -> Self
333 where
334 S: Into<Cow<'a, str>>,
335 {
336 self.response_types_supported = Some(types.into_iter().map(|s| s.into()).collect());
337 self
338 }
339
340 pub fn response_modes_supported<S>(mut self, modes: Vec<S>) -> Self
341 where
342 S: Into<Cow<'a, str>>,
343 {
344 self.response_modes_supported = Some(modes.into_iter().map(|s| s.into()).collect());
345 self
346 }
347
348 pub fn registration_endpoint(mut self, url: &'a str) -> Self {
349 self.registration_endpoint = Some(url.into());
350 self
351 }
352
353 pub fn userinfo_endpoint(mut self, url: &'a str) -> Self {
354 self.userinfo_endpoint = Some(url.into());
355 self
356 }
357
358 pub fn grant_types_supported<S>(mut self, types: Vec<S>) -> Self
359 where
360 S: Into<Cow<'a, str>>,
361 {
362 self.grant_types_supported = Some(types.into_iter().map(|s| s.into()).collect());
363 self
364 }
365
366 pub fn token_endpoint_auth_methods_supported<S>(mut self, methods: Vec<S>) -> Self
367 where
368 S: Into<Cow<'a, str>>,
369 {
370 self.token_endpoint_auth_methods_supported =
371 Some(methods.into_iter().map(|s| s.into()).collect());
372 self
373 }
374
375 pub fn token_endpoint_auth_signing_alg_values_supported<S>(mut self, algs: Vec<S>) -> Self
376 where
377 S: Into<Cow<'a, str>>,
378 {
379 self.token_endpoint_auth_signing_alg_values_supported =
380 Some(algs.into_iter().map(|s| s.into()).collect());
381 self
382 }
383
384 pub fn revocation_endpoint(mut self, url: &'a str) -> Self {
385 self.revocation_endpoint = Some(url.into());
386 self
387 }
388
389 pub fn revocation_endpoint_auth_methods_supported<S>(mut self, methods: Vec<S>) -> Self
390 where
391 S: Into<Cow<'a, str>>,
392 {
393 self.revocation_endpoint_auth_methods_supported =
394 Some(methods.into_iter().map(|s| s.into()).collect());
395 self
396 }
397
398 pub fn revocation_endpoint_auth_signing_alg_values_supported<S>(mut self, algs: Vec<S>) -> Self
399 where
400 S: Into<Cow<'a, str>>,
401 {
402 self.revocation_endpoint_auth_signing_alg_values_supported =
403 Some(algs.into_iter().map(|s| s.into()).collect());
404 self
405 }
406
407 pub fn introspection_endpoint(mut self, endpoint: &'a str) -> Self {
408 self.introspection_endpoint = Some(endpoint.into());
409 self
410 }
411
412 pub fn introspection_endpoint_auth_methods_supported<S>(mut self, methods: Vec<S>) -> Self
413 where
414 S: Into<Cow<'a, str>>,
415 {
416 self.introspection_endpoint_auth_methods_supported =
417 Some(methods.into_iter().map(|s| s.into()).collect());
418 self
419 }
420
421 pub fn introspection_endpoint_auth_signing_alg_values_supported<S>(
422 mut self,
423 algs: Vec<String>,
424 ) -> Self
425 where
426 S: Into<Cow<'a, str>>,
427 {
428 self.introspection_endpoint_auth_signing_alg_values_supported =
429 Some(algs.into_iter().map(|s| s.into()).collect());
430 self
431 }
432
433 pub fn code_challenge_methods_supported<S>(mut self, methods: Vec<S>) -> Self
434 where
435 S: Into<Cow<'a, str>>,
436 {
437 self.code_challenge_methods_supported =
438 Some(methods.into_iter().map(|s| s.into()).collect());
439 self
440 }
441
442 pub fn resource(mut self, url: &'a str) -> Self {
444 self.resource = Some(url.into());
445 self
446 }
447
448 pub fn authorization_servers(mut self, servers: Vec<&'a str>) -> Self {
449 self.authorization_servers = Some(servers.into_iter().map(|s| s.into()).collect());
450 self
451 }
452
453 pub fn reqquired_scopes<S>(mut self, scopes: Vec<S>) -> Self
454 where
455 S: Into<Cow<'a, str>>,
456 {
457 self.required_scopes = Some(scopes.into_iter().map(|s| s.into()).collect());
458 self
459 }
460
461 pub fn resource_documentation<S>(mut self, doc: String) -> Self
462 where
463 S: Into<Cow<'a, str>>,
464 {
465 self.resource_documentation = Some(doc.into());
466 self
467 }
468
469 pub fn jwks_uri(mut self, url: &'a str) -> Self {
470 self.jwks_uri = Some(url.into());
471 self
472 }
473
474 pub fn bearer_methods_supported<S>(mut self, methods: Vec<S>) -> Self
475 where
476 S: Into<Cow<'a, str>>,
477 {
478 self.bearer_methods_supported = Some(methods.into_iter().map(|s| s.into()).collect());
479 self
480 }
481
482 pub fn resource_signing_alg_values_supported<S>(mut self, algs: Vec<S>) -> Self
483 where
484 S: Into<Cow<'a, str>>,
485 {
486 self.resource_signing_alg_values_supported =
487 Some(algs.into_iter().map(|s| s.into()).collect());
488 self
489 }
490
491 pub fn resource_name<S>(mut self, name: S) -> Self
492 where
493 S: Into<Cow<'a, str>>,
494 {
495 self.resource_name = Some(name.into());
496 self
497 }
498
499 pub fn resource_policy_uri(mut self, url: &'a str) -> Self {
500 self.resource_policy_uri = Some(url.into());
501 self
502 }
503
504 pub fn resource_tos_uri(mut self, url: &'a str) -> Self {
505 self.resource_tos_uri = Some(url.into());
506 self
507 }
508
509 pub fn tls_client_certificate_bound_access_tokens(mut self, value: bool) -> Self {
510 self.tls_client_certificate_bound_access_tokens = Some(value);
511 self
512 }
513
514 pub fn authorization_details_types_supported<S>(mut self, types: Vec<S>) -> Self
515 where
516 S: Into<Cow<'a, str>>,
517 {
518 self.authorization_details_types_supported =
519 Some(types.into_iter().map(|s| s.into()).collect());
520 self
521 }
522
523 pub fn dpop_signing_alg_values_supported<S>(mut self, algs: Vec<S>) -> Self
524 where
525 S: Into<Cow<'a, str>>,
526 {
527 self.dpop_signing_alg_values_supported = Some(algs.into_iter().map(|s| s.into()).collect());
528 self
529 }
530
531 pub fn dpop_bound_access_tokens_required(mut self, value: bool) -> Self {
532 self.dpop_bound_access_tokens_required = Some(value);
533 self
534 }
535
536 pub fn build(
538 self,
539 ) -> Result<(AuthorizationServerMetadata, OauthProtectedResourceMetadata), McpSdkError> {
540 let issuer = Self::parse_url_field("issuer", self.issuer, None)?;
541
542 let authorization_endpoint = Self::parse_url_field(
543 "authorization_endpoint",
544 self.authorization_endpoint,
545 Some(&issuer),
546 )?;
547
548 let token_endpoint =
549 Self::parse_url_field("token_endpoint", self.token_endpoint, Some(&issuer))?;
550
551 let registration_endpoint = Self::parse_optional_url_field(
552 "registration_endpoint",
553 self.registration_endpoint,
554 Some(&issuer),
555 )?;
556
557 let revocation_endpoint = Self::parse_optional_url_field(
558 "revocation_endpoint",
559 self.revocation_endpoint,
560 Some(&issuer),
561 )?;
562
563 let introspection_endpoint = Self::parse_optional_url_field(
564 "introspection_endpoint",
565 self.introspection_endpoint,
566 Some(&issuer),
567 )?;
568
569 let service_documentation = Self::parse_optional_url_field(
570 "service_documentation",
571 self.service_documentation,
572 None,
573 )?;
574
575 let jwks_uri = Self::parse_optional_url_field("jwks_uri", self.jwks_uri, Some(&issuer))?;
576
577 let authorization_server_metadata = AuthorizationServerMetadata {
578 issuer,
579 authorization_endpoint,
580 token_endpoint,
581 registration_endpoint,
582 service_documentation,
583 revocation_endpoint,
584 introspection_endpoint,
585 userinfo_endpoint: self.userinfo_endpoint.map(|v| v.into()),
586 response_types_supported: self
587 .response_types_supported
588 .unwrap_or_default()
589 .into_iter() .map(|c| c.into_owned())
591 .collect(),
592 response_modes_supported: self
593 .response_modes_supported
594 .map(|v| v.into_iter().map(|c| c.into_owned()).collect()),
595 scopes_supported: self
596 .scopes_supported
597 .map(|v| v.into_iter().map(|c| c.into_owned()).collect()),
598 grant_types_supported: self
599 .grant_types_supported
600 .map(|v| v.into_iter().map(|c| c.into_owned()).collect()),
601 token_endpoint_auth_methods_supported: self
602 .token_endpoint_auth_methods_supported
603 .map(|v| v.into_iter().map(|c| c.into_owned()).collect()),
604 token_endpoint_auth_signing_alg_values_supported: self
605 .token_endpoint_auth_signing_alg_values_supported
606 .map(|v| v.into_iter().map(|c| c.into_owned()).collect()),
607 revocation_endpoint_auth_signing_alg_values_supported: self
608 .revocation_endpoint_auth_signing_alg_values_supported
609 .map(|v| v.into_iter().map(|c| c.into_owned()).collect()),
610 revocation_endpoint_auth_methods_supported: self
611 .revocation_endpoint_auth_methods_supported
612 .map(|v| v.into_iter().map(|c| c.into_owned()).collect()),
613 introspection_endpoint_auth_methods_supported: self
614 .introspection_endpoint_auth_methods_supported
615 .map(|v| v.into_iter().map(|c| c.into_owned()).collect()),
616 introspection_endpoint_auth_signing_alg_values_supported: self
617 .introspection_endpoint_auth_signing_alg_values_supported
618 .map(|v| v.into_iter().map(|c| c.into_owned()).collect()),
619 code_challenge_methods_supported: self
620 .code_challenge_methods_supported
621 .map(|v| v.into_iter().map(|c| c.into_owned()).collect()),
622 jwks_uri: jwks_uri.clone(),
623 client_id_metadata_document_supported: None,
624 };
625
626 let resource = Self::parse_url_field("resource", self.resource, None)?;
627 let resource_policy_uri =
628 Self::parse_optional_url_field("resource_policy_uri", self.resource_policy_uri, None)?;
629 let resource_tos_uri =
630 Self::parse_optional_url_field("resource_tos_uri", self.resource_tos_uri, None)?;
631
632 let authorization_servers =
634 self.authorization_servers
635 .ok_or_else(|| McpSdkError::Internal {
636 description: "Error: 'authorization_servers' is missing".to_string(),
637 })?;
638 if authorization_servers.is_empty() {
639 return Err(McpSdkError::Internal {
640 description: "Error: 'authorization_servers' must contain at least one URL"
641 .to_string(),
642 });
643 }
644 let authorization_servers = authorization_servers
645 .iter()
646 .map(|url| {
647 Url::parse(url).map_err(|err| McpSdkError::Internal {
648 description: format!(
649 "Error: 'authorization_servers' contains invalid URL '{url}': {err}",
650 ),
651 })
652 })
653 .collect::<Result<Vec<_>, _>>()?;
654
655 let protected_resource_metadata = OauthProtectedResourceMetadata {
656 resource,
657 authorization_servers,
658 jwks_uri,
659 scopes_supported: self
660 .required_scopes
661 .map(|v| v.into_iter().map(|c| c.into_owned()).collect()),
662 bearer_methods_supported: self
663 .bearer_methods_supported
664 .map(|v| v.into_iter().map(|c| c.into_owned()).collect()),
665 resource_signing_alg_values_supported: self
666 .resource_signing_alg_values_supported
667 .map(|v| v.into_iter().map(|c| c.into_owned()).collect()),
668 resource_name: self.resource_name.map(|s| s.into()),
669 resource_documentation: self.resource_documentation.map(|s| s.into()),
670 resource_policy_uri,
671 resource_tos_uri,
672 tls_client_certificate_bound_access_tokens: self
673 .tls_client_certificate_bound_access_tokens,
674 authorization_details_types_supported: self
675 .authorization_details_types_supported
676 .map(|v| v.into_iter().map(|c| c.into_owned()).collect()),
677 dpop_signing_alg_values_supported: self
678 .dpop_signing_alg_values_supported
679 .map(|v| v.into_iter().map(|c| c.into_owned()).collect()),
680 dpop_bound_access_tokens_required: self.dpop_bound_access_tokens_required,
681 };
682
683 Ok((authorization_server_metadata, protected_resource_metadata))
684 }
685}