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