1use serde::{Deserialize, Serialize};
36use std::collections::HashMap;
37use std::time::{Duration, SystemTime};
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum OAuthError {
43 InvalidRequest,
45 UnauthorizedClient,
47 AccessDenied,
49 UnsupportedResponseType,
51 InvalidScope,
53 ServerError,
55 TemporarilyUnavailable,
57 InvalidGrant,
59 InvalidClient,
61 UnsupportedGrantType,
63 InvalidToken,
65 InsufficientScope,
67}
68
69impl std::fmt::Display for OAuthError {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 match self {
72 Self::InvalidRequest => write!(f, "invalid_request"),
73 Self::UnauthorizedClient => write!(f, "unauthorized_client"),
74 Self::AccessDenied => write!(f, "access_denied"),
75 Self::UnsupportedResponseType => write!(f, "unsupported_response_type"),
76 Self::InvalidScope => write!(f, "invalid_scope"),
77 Self::ServerError => write!(f, "server_error"),
78 Self::TemporarilyUnavailable => write!(f, "temporarily_unavailable"),
79 Self::InvalidGrant => write!(f, "invalid_grant"),
80 Self::InvalidClient => write!(f, "invalid_client"),
81 Self::UnsupportedGrantType => write!(f, "unsupported_grant_type"),
82 Self::InvalidToken => write!(f, "invalid_token"),
83 Self::InsufficientScope => write!(f, "insufficient_scope"),
84 }
85 }
86}
87
88impl std::error::Error for OAuthError {}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct OAuthErrorResponse {
93 pub error: OAuthError,
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub error_description: Option<String>,
98 #[serde(skip_serializing_if = "Option::is_none")]
100 pub error_uri: Option<String>,
101}
102
103impl OAuthErrorResponse {
104 #[must_use]
106 pub const fn new(error: OAuthError) -> Self {
107 Self {
108 error,
109 error_description: None,
110 error_uri: None,
111 }
112 }
113
114 #[must_use]
116 pub fn with_description(mut self, description: impl Into<String>) -> Self {
117 self.error_description = Some(description.into());
118 self
119 }
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct ProtectedResourceMetadata {
128 pub resource: String,
130
131 pub authorization_servers: Vec<String>,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub bearer_methods_supported: Option<Vec<String>>,
138
139 #[serde(skip_serializing_if = "Option::is_none")]
141 pub resource_documentation: Option<String>,
142
143 #[serde(skip_serializing_if = "Option::is_none")]
145 pub scopes_supported: Option<Vec<String>>,
146
147 #[serde(skip_serializing_if = "Option::is_none")]
149 pub resource_signing_alg_values_supported: Option<Vec<String>>,
150
151 #[serde(flatten)]
153 pub extra: HashMap<String, serde_json::Value>,
154}
155
156impl ProtectedResourceMetadata {
157 #[must_use]
163 pub fn new(resource: impl Into<String>) -> Self {
164 Self {
165 resource: resource.into(),
166 authorization_servers: Vec::new(),
167 bearer_methods_supported: Some(vec!["header".to_string()]),
168 resource_documentation: None,
169 scopes_supported: None,
170 resource_signing_alg_values_supported: None,
171 extra: HashMap::new(),
172 }
173 }
174
175 #[must_use]
177 pub fn with_authorization_server(mut self, server: impl Into<String>) -> Self {
178 self.authorization_servers.push(server.into());
179 self
180 }
181
182 #[must_use]
184 pub fn with_scopes(mut self, scopes: impl IntoIterator<Item = impl Into<String>>) -> Self {
185 self.scopes_supported = Some(scopes.into_iter().map(Into::into).collect());
186 self
187 }
188
189 #[must_use]
191 pub fn with_documentation(mut self, url: impl Into<String>) -> Self {
192 self.resource_documentation = Some(url.into());
193 self
194 }
195
196 pub fn validate(&self) -> Result<(), String> {
198 if self.resource.is_empty() {
199 return Err("Resource identifier is required".to_string());
200 }
201 if self.authorization_servers.is_empty() {
202 return Err(
203 "At least one authorization server is required per MCP specification".to_string(),
204 );
205 }
206 Ok(())
207 }
208
209 #[must_use]
211 pub fn well_known_url(resource_url: &str) -> Option<String> {
212 url::Url::parse(resource_url).ok().map(|url| {
214 format!(
215 "{}://{}/.well-known/oauth-protected-resource",
216 url.scheme(),
217 url.host_str().unwrap_or("localhost")
218 )
219 })
220 }
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct AuthorizationServerMetadata {
226 pub issuer: String,
228
229 pub authorization_endpoint: String,
231
232 pub token_endpoint: String,
234
235 #[serde(skip_serializing_if = "Option::is_none")]
237 pub jwks_uri: Option<String>,
238
239 #[serde(skip_serializing_if = "Option::is_none")]
241 pub registration_endpoint: Option<String>,
242
243 #[serde(skip_serializing_if = "Option::is_none")]
245 pub scopes_supported: Option<Vec<String>>,
246
247 #[serde(skip_serializing_if = "Option::is_none")]
249 pub response_types_supported: Option<Vec<String>>,
250
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub grant_types_supported: Option<Vec<String>>,
254
255 #[serde(skip_serializing_if = "Option::is_none")]
257 pub token_endpoint_auth_methods_supported: Option<Vec<String>>,
258
259 #[serde(skip_serializing_if = "Option::is_none")]
261 pub code_challenge_methods_supported: Option<Vec<String>>,
262
263 #[serde(skip_serializing_if = "Option::is_none")]
265 pub revocation_endpoint: Option<String>,
266
267 #[serde(skip_serializing_if = "Option::is_none")]
269 pub introspection_endpoint: Option<String>,
270
271 #[serde(flatten)]
273 pub extra: HashMap<String, serde_json::Value>,
274}
275
276impl AuthorizationServerMetadata {
277 #[must_use]
279 pub fn new(
280 issuer: impl Into<String>,
281 authorization_endpoint: impl Into<String>,
282 token_endpoint: impl Into<String>,
283 ) -> Self {
284 Self {
285 issuer: issuer.into(),
286 authorization_endpoint: authorization_endpoint.into(),
287 token_endpoint: token_endpoint.into(),
288 jwks_uri: None,
289 registration_endpoint: None,
290 scopes_supported: None,
291 response_types_supported: Some(vec!["code".to_string()]),
292 grant_types_supported: Some(vec![
293 "authorization_code".to_string(),
294 "client_credentials".to_string(),
295 "refresh_token".to_string(),
296 ]),
297 token_endpoint_auth_methods_supported: None,
298 code_challenge_methods_supported: Some(vec!["S256".to_string()]),
299 revocation_endpoint: None,
300 introspection_endpoint: None,
301 extra: HashMap::new(),
302 }
303 }
304
305 #[must_use]
307 pub fn from_issuer(issuer: impl Into<String>) -> Self {
308 let issuer = issuer.into();
309 Self::new(
310 &issuer,
311 format!("{issuer}/authorize"),
312 format!("{issuer}/token"),
313 )
314 }
315
316 #[must_use]
318 pub fn with_jwks_uri(mut self, uri: impl Into<String>) -> Self {
319 self.jwks_uri = Some(uri.into());
320 self
321 }
322
323 #[must_use]
325 pub fn with_registration_endpoint(mut self, endpoint: impl Into<String>) -> Self {
326 self.registration_endpoint = Some(endpoint.into());
327 self
328 }
329
330 #[must_use]
332 pub fn well_known_url(issuer: &str) -> Option<String> {
333 url::Url::parse(issuer).ok().map(|url| {
334 format!(
335 "{}://{}/.well-known/oauth-authorization-server",
336 url.scheme(),
337 url.host_str().unwrap_or("localhost")
338 )
339 })
340 }
341}
342
343#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
345#[serde(rename_all = "snake_case")]
346pub enum GrantType {
347 AuthorizationCode,
349 ClientCredentials,
351 RefreshToken,
353}
354
355impl std::fmt::Display for GrantType {
356 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
357 match self {
358 Self::AuthorizationCode => write!(f, "authorization_code"),
359 Self::ClientCredentials => write!(f, "client_credentials"),
360 Self::RefreshToken => write!(f, "refresh_token"),
361 }
362 }
363}
364
365#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
367pub enum CodeChallengeMethod {
368 #[default]
370 S256,
371 Plain,
373}
374
375impl std::fmt::Display for CodeChallengeMethod {
376 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
377 match self {
378 Self::S256 => write!(f, "S256"),
379 Self::Plain => write!(f, "plain"),
380 }
381 }
382}
383
384#[derive(Debug, Clone)]
386pub struct PkceChallenge {
387 pub verifier: String,
389 pub challenge: String,
391 pub method: CodeChallengeMethod,
393}
394
395impl PkceChallenge {
396 #[must_use]
398 pub fn new() -> Self {
399 Self::with_method(CodeChallengeMethod::S256)
400 }
401
402 #[must_use]
404 pub fn with_method(method: CodeChallengeMethod) -> Self {
405 use base64::Engine;
406 use rand::Rng;
407
408 let mut rng = rand::thread_rng();
410 let verifier_bytes: [u8; 32] = rng.r#gen();
411 let verifier = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(verifier_bytes);
412
413 let challenge = match method {
414 CodeChallengeMethod::S256 => {
415 use sha2::{Digest, Sha256};
416 let hash = Sha256::digest(verifier.as_bytes());
417 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hash)
418 }
419 CodeChallengeMethod::Plain => verifier.clone(),
420 };
421
422 Self {
423 verifier,
424 challenge,
425 method,
426 }
427 }
428
429 #[must_use]
431 pub fn verify(verifier: &str, challenge: &str, method: CodeChallengeMethod) -> bool {
432 use base64::Engine;
433
434 let computed = match method {
435 CodeChallengeMethod::S256 => {
436 use sha2::{Digest, Sha256};
437 let hash = Sha256::digest(verifier.as_bytes());
438 base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hash)
439 }
440 CodeChallengeMethod::Plain => verifier.to_string(),
441 };
442
443 computed == challenge
444 }
445}
446
447impl Default for PkceChallenge {
448 fn default() -> Self {
449 Self::new()
450 }
451}
452
453#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct AuthorizationRequest {
456 pub response_type: String,
458 pub client_id: String,
460 #[serde(skip_serializing_if = "Option::is_none")]
462 pub redirect_uri: Option<String>,
463 #[serde(skip_serializing_if = "Option::is_none")]
465 pub scope: Option<String>,
466 #[serde(skip_serializing_if = "Option::is_none")]
468 pub state: Option<String>,
469 pub code_challenge: String,
471 pub code_challenge_method: String,
473 pub resource: String,
475}
476
477impl AuthorizationRequest {
478 #[must_use]
480 pub fn new(
481 client_id: impl Into<String>,
482 pkce: &PkceChallenge,
483 resource: impl Into<String>,
484 ) -> Self {
485 Self {
486 response_type: "code".to_string(),
487 client_id: client_id.into(),
488 redirect_uri: None,
489 scope: None,
490 state: None,
491 code_challenge: pkce.challenge.clone(),
492 code_challenge_method: pkce.method.to_string(),
493 resource: resource.into(),
494 }
495 }
496
497 #[must_use]
499 pub fn with_redirect_uri(mut self, uri: impl Into<String>) -> Self {
500 self.redirect_uri = Some(uri.into());
501 self
502 }
503
504 #[must_use]
506 pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
507 self.scope = Some(scope.into());
508 self
509 }
510
511 #[must_use]
513 pub fn with_state(mut self, state: impl Into<String>) -> Self {
514 self.state = Some(state.into());
515 self
516 }
517
518 #[must_use]
520 pub fn build_url(&self, authorization_endpoint: &str) -> Option<String> {
521 let mut url = url::Url::parse(authorization_endpoint).ok()?;
522
523 {
524 let mut query = url.query_pairs_mut();
525 query.append_pair("response_type", &self.response_type);
526 query.append_pair("client_id", &self.client_id);
527 query.append_pair("code_challenge", &self.code_challenge);
528 query.append_pair("code_challenge_method", &self.code_challenge_method);
529 query.append_pair("resource", &self.resource);
530
531 if let Some(ref uri) = self.redirect_uri {
532 query.append_pair("redirect_uri", uri);
533 }
534 if let Some(ref scope) = self.scope {
535 query.append_pair("scope", scope);
536 }
537 if let Some(ref state) = self.state {
538 query.append_pair("state", state);
539 }
540 }
541
542 Some(url.to_string())
543 }
544}
545
546#[derive(Debug, Clone, Serialize, Deserialize)]
548pub struct TokenRequest {
549 pub grant_type: String,
551 #[serde(skip_serializing_if = "Option::is_none")]
553 pub code: Option<String>,
554 #[serde(skip_serializing_if = "Option::is_none")]
556 pub redirect_uri: Option<String>,
557 pub client_id: String,
559 #[serde(skip_serializing_if = "Option::is_none")]
561 pub client_secret: Option<String>,
562 #[serde(skip_serializing_if = "Option::is_none")]
564 pub code_verifier: Option<String>,
565 #[serde(skip_serializing_if = "Option::is_none")]
567 pub resource: Option<String>,
568 #[serde(skip_serializing_if = "Option::is_none")]
570 pub refresh_token: Option<String>,
571 #[serde(skip_serializing_if = "Option::is_none")]
573 pub scope: Option<String>,
574}
575
576impl TokenRequest {
577 #[must_use]
579 pub fn authorization_code(
580 code: impl Into<String>,
581 client_id: impl Into<String>,
582 code_verifier: impl Into<String>,
583 resource: impl Into<String>,
584 ) -> Self {
585 Self {
586 grant_type: "authorization_code".to_string(),
587 code: Some(code.into()),
588 redirect_uri: None,
589 client_id: client_id.into(),
590 client_secret: None,
591 code_verifier: Some(code_verifier.into()),
592 resource: Some(resource.into()),
593 refresh_token: None,
594 scope: None,
595 }
596 }
597
598 #[must_use]
600 pub fn client_credentials(
601 client_id: impl Into<String>,
602 client_secret: impl Into<String>,
603 resource: impl Into<String>,
604 ) -> Self {
605 Self {
606 grant_type: "client_credentials".to_string(),
607 code: None,
608 redirect_uri: None,
609 client_id: client_id.into(),
610 client_secret: Some(client_secret.into()),
611 code_verifier: None,
612 resource: Some(resource.into()),
613 refresh_token: None,
614 scope: None,
615 }
616 }
617
618 #[must_use]
620 pub fn refresh(refresh_token: impl Into<String>, client_id: impl Into<String>) -> Self {
621 Self {
622 grant_type: "refresh_token".to_string(),
623 code: None,
624 redirect_uri: None,
625 client_id: client_id.into(),
626 client_secret: None,
627 code_verifier: None,
628 resource: None,
629 refresh_token: Some(refresh_token.into()),
630 scope: None,
631 }
632 }
633
634 #[must_use]
636 pub fn with_redirect_uri(mut self, uri: impl Into<String>) -> Self {
637 self.redirect_uri = Some(uri.into());
638 self
639 }
640
641 #[must_use]
643 pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
644 self.scope = Some(scope.into());
645 self
646 }
647}
648
649#[derive(Debug, Clone, Serialize, Deserialize)]
651pub struct TokenResponse {
652 pub access_token: String,
654 pub token_type: String,
656 #[serde(skip_serializing_if = "Option::is_none")]
658 pub expires_in: Option<u64>,
659 #[serde(skip_serializing_if = "Option::is_none")]
661 pub refresh_token: Option<String>,
662 #[serde(skip_serializing_if = "Option::is_none")]
664 pub scope: Option<String>,
665}
666
667impl TokenResponse {
668 #[must_use]
670 pub fn is_expired(&self, issued_at: SystemTime) -> bool {
671 if let Some(expires_in) = self.expires_in {
672 let expiry = issued_at + Duration::from_secs(expires_in);
673 SystemTime::now() >= expiry
674 } else {
675 false }
677 }
678}
679
680#[derive(Debug, Clone)]
682pub struct StoredToken {
683 pub token: TokenResponse,
685 pub issued_at: SystemTime,
687 pub resource: String,
689}
690
691impl StoredToken {
692 #[must_use]
694 pub fn new(token: TokenResponse, resource: impl Into<String>) -> Self {
695 Self {
696 token,
697 issued_at: SystemTime::now(),
698 resource: resource.into(),
699 }
700 }
701
702 #[must_use]
704 pub fn is_expired(&self) -> bool {
705 self.token.is_expired(self.issued_at)
706 }
707
708 #[must_use]
710 pub fn expires_within(&self, duration: Duration) -> bool {
711 if let Some(expires_in) = self.token.expires_in {
712 let expiry = self.issued_at + Duration::from_secs(expires_in);
713 SystemTime::now() + duration >= expiry
714 } else {
715 false
716 }
717 }
718
719 #[must_use]
721 pub fn authorization_header(&self) -> String {
722 format!("Bearer {}", self.token.access_token)
723 }
724}
725
726#[derive(Debug, Clone)]
728pub struct WwwAuthenticate {
729 pub realm: Option<String>,
731 pub resource_metadata: String,
733 pub error: Option<OAuthError>,
735 pub error_description: Option<String>,
737}
738
739impl WwwAuthenticate {
740 #[must_use]
742 pub fn new(resource_metadata: impl Into<String>) -> Self {
743 Self {
744 realm: None,
745 resource_metadata: resource_metadata.into(),
746 error: None,
747 error_description: None,
748 }
749 }
750
751 #[must_use]
753 pub fn with_realm(mut self, realm: impl Into<String>) -> Self {
754 self.realm = Some(realm.into());
755 self
756 }
757
758 #[must_use]
760 pub const fn with_error(mut self, error: OAuthError) -> Self {
761 self.error = Some(error);
762 self
763 }
764
765 #[must_use]
767 pub fn with_error_description(mut self, description: impl Into<String>) -> Self {
768 self.error_description = Some(description.into());
769 self
770 }
771
772 #[must_use]
774 pub fn to_header_value(&self) -> String {
775 let mut parts = vec![format!(
776 "Bearer resource_metadata=\"{}\"",
777 self.resource_metadata
778 )];
779
780 if let Some(ref realm) = self.realm {
781 parts.push(format!("realm=\"{realm}\""));
782 }
783 if let Some(ref error) = self.error {
784 parts.push(format!("error=\"{error}\""));
785 }
786 if let Some(ref desc) = self.error_description {
787 parts.push(format!("error_description=\"{desc}\""));
788 }
789
790 parts.join(", ")
791 }
792
793 #[must_use]
795 pub fn parse(header_value: &str) -> Option<Self> {
796 if !header_value.starts_with("Bearer ") {
797 return None;
798 }
799
800 let params = &header_value[7..]; let mut resource_metadata = None;
802 let mut realm = None;
803 let mut error = None;
804 let mut error_description = None;
805
806 for part in params.split(", ") {
807 if let Some((key, value)) = part.split_once('=') {
808 let value = value.trim_matches('"');
809 match key.trim() {
810 "resource_metadata" => resource_metadata = Some(value.to_string()),
811 "realm" => realm = Some(value.to_string()),
812 "error" => {
813 error = match value {
814 "invalid_request" => Some(OAuthError::InvalidRequest),
815 "invalid_token" => Some(OAuthError::InvalidToken),
816 "insufficient_scope" => Some(OAuthError::InsufficientScope),
817 _ => None,
818 };
819 }
820 "error_description" => error_description = Some(value.to_string()),
821 _ => {}
822 }
823 }
824 }
825
826 resource_metadata.map(|rm| Self {
827 realm,
828 resource_metadata: rm,
829 error,
830 error_description,
831 })
832 }
833}
834
835#[derive(Debug, Clone)]
837pub struct AuthorizationConfig {
838 pub authorization_server: String,
840 pub client_id: String,
842 pub client_secret: Option<String>,
844 pub redirect_uri: Option<String>,
846 pub resource: Option<String>,
848 pub scopes: Vec<String>,
850}
851
852impl AuthorizationConfig {
853 #[must_use]
855 pub fn new(authorization_server: impl Into<String>) -> Self {
856 Self {
857 authorization_server: authorization_server.into(),
858 client_id: String::new(),
859 client_secret: None,
860 redirect_uri: None,
861 resource: None,
862 scopes: Vec::new(),
863 }
864 }
865
866 #[must_use]
868 pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
869 self.client_id = client_id.into();
870 self
871 }
872
873 #[must_use]
875 pub fn with_client_secret(mut self, secret: impl Into<String>) -> Self {
876 self.client_secret = Some(secret.into());
877 self
878 }
879
880 #[must_use]
882 pub fn with_redirect_uri(mut self, uri: impl Into<String>) -> Self {
883 self.redirect_uri = Some(uri.into());
884 self
885 }
886
887 #[must_use]
889 pub fn with_resource(mut self, resource: impl Into<String>) -> Self {
890 self.resource = Some(resource.into());
891 self
892 }
893
894 #[must_use]
896 pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
897 self.scopes.push(scope.into());
898 self
899 }
900
901 #[must_use]
903 pub const fn is_public_client(&self) -> bool {
904 self.client_secret.is_none()
905 }
906}
907
908#[derive(Debug, Clone, Serialize, Deserialize)]
910pub struct ClientRegistrationRequest {
911 #[serde(skip_serializing_if = "Option::is_none")]
913 pub redirect_uris: Option<Vec<String>>,
914 #[serde(skip_serializing_if = "Option::is_none")]
916 pub token_endpoint_auth_method: Option<String>,
917 #[serde(skip_serializing_if = "Option::is_none")]
919 pub grant_types: Option<Vec<String>>,
920 #[serde(skip_serializing_if = "Option::is_none")]
922 pub response_types: Option<Vec<String>>,
923 #[serde(skip_serializing_if = "Option::is_none")]
925 pub client_name: Option<String>,
926 #[serde(skip_serializing_if = "Option::is_none")]
928 pub client_uri: Option<String>,
929 #[serde(skip_serializing_if = "Option::is_none")]
931 pub software_id: Option<String>,
932 #[serde(skip_serializing_if = "Option::is_none")]
934 pub software_version: Option<String>,
935}
936
937impl ClientRegistrationRequest {
938 #[must_use]
940 pub fn new() -> Self {
941 Self {
942 redirect_uris: None,
943 token_endpoint_auth_method: Some("none".to_string()), grant_types: Some(vec!["authorization_code".to_string()]),
945 response_types: Some(vec!["code".to_string()]),
946 client_name: None,
947 client_uri: None,
948 software_id: None,
949 software_version: None,
950 }
951 }
952
953 #[must_use]
955 pub fn with_redirect_uris(mut self, uris: impl IntoIterator<Item = impl Into<String>>) -> Self {
956 self.redirect_uris = Some(uris.into_iter().map(Into::into).collect());
957 self
958 }
959
960 #[must_use]
962 pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
963 self.client_name = Some(name.into());
964 self
965 }
966
967 #[must_use]
969 pub fn with_software_id(mut self, id: impl Into<String>) -> Self {
970 self.software_id = Some(id.into());
971 self
972 }
973}
974
975impl Default for ClientRegistrationRequest {
976 fn default() -> Self {
977 Self::new()
978 }
979}
980
981#[derive(Debug, Clone, Serialize, Deserialize)]
983pub struct ClientRegistrationResponse {
984 pub client_id: String,
986 #[serde(skip_serializing_if = "Option::is_none")]
988 pub client_secret: Option<String>,
989 #[serde(skip_serializing_if = "Option::is_none")]
991 pub client_secret_expires_at: Option<u64>,
992 #[serde(skip_serializing_if = "Option::is_none")]
994 pub client_id_issued_at: Option<u64>,
995 #[serde(flatten)]
997 pub metadata: HashMap<String, serde_json::Value>,
998}
999
1000#[cfg(test)]
1001mod tests {
1002 use super::*;
1003
1004 #[test]
1005 fn test_protected_resource_metadata() {
1006 let metadata = ProtectedResourceMetadata::new("https://mcp.example.com")
1007 .with_authorization_server("https://auth.example.com")
1008 .with_scopes(["mcp:read", "mcp:write"]);
1009
1010 assert_eq!(metadata.resource, "https://mcp.example.com");
1011 assert_eq!(metadata.authorization_servers.len(), 1);
1012 assert!(metadata.validate().is_ok());
1013 }
1014
1015 #[test]
1016 fn test_protected_resource_metadata_validation() {
1017 let metadata = ProtectedResourceMetadata::new("https://mcp.example.com");
1018 assert!(metadata.validate().is_err()); let metadata = metadata.with_authorization_server("https://auth.example.com");
1021 assert!(metadata.validate().is_ok());
1022 }
1023
1024 #[test]
1025 fn test_authorization_server_metadata() {
1026 let metadata = AuthorizationServerMetadata::from_issuer("https://auth.example.com");
1027
1028 assert_eq!(metadata.issuer, "https://auth.example.com");
1029 assert_eq!(
1030 metadata.authorization_endpoint,
1031 "https://auth.example.com/authorize"
1032 );
1033 assert_eq!(metadata.token_endpoint, "https://auth.example.com/token");
1034 }
1035
1036 #[test]
1037 fn test_pkce_challenge() {
1038 let pkce = PkceChallenge::new();
1039
1040 assert_ne!(pkce.verifier, pkce.challenge);
1042
1043 assert!(PkceChallenge::verify(
1045 &pkce.verifier,
1046 &pkce.challenge,
1047 CodeChallengeMethod::S256
1048 ));
1049
1050 assert!(!PkceChallenge::verify(
1052 "wrong",
1053 &pkce.challenge,
1054 CodeChallengeMethod::S256
1055 ));
1056 }
1057
1058 #[test]
1059 fn test_authorization_request() {
1060 let pkce = PkceChallenge::new();
1061 let request = AuthorizationRequest::new("client123", &pkce, "https://mcp.example.com")
1062 .with_redirect_uri("http://localhost:8080/callback")
1063 .with_scope("mcp:read")
1064 .with_state("random_state");
1065
1066 assert_eq!(request.client_id, "client123");
1067 assert_eq!(request.resource, "https://mcp.example.com");
1068
1069 let url = request.build_url("https://auth.example.com/authorize");
1070 assert!(url.is_some());
1071 let url = url.unwrap();
1072 assert!(url.contains("response_type=code"));
1073 assert!(url.contains("client_id=client123"));
1074 assert!(url.contains("resource="));
1075 }
1076
1077 #[test]
1078 fn test_token_request_authorization_code() {
1079 let request = TokenRequest::authorization_code(
1080 "auth_code_123",
1081 "client123",
1082 "verifier123",
1083 "https://mcp.example.com",
1084 );
1085
1086 assert_eq!(request.grant_type, "authorization_code");
1087 assert_eq!(request.code, Some("auth_code_123".to_string()));
1088 assert_eq!(request.code_verifier, Some("verifier123".to_string()));
1089 }
1090
1091 #[test]
1092 fn test_token_request_client_credentials() {
1093 let request =
1094 TokenRequest::client_credentials("client123", "secret456", "https://mcp.example.com");
1095
1096 assert_eq!(request.grant_type, "client_credentials");
1097 assert_eq!(request.client_secret, Some("secret456".to_string()));
1098 }
1099
1100 #[test]
1101 fn test_www_authenticate_header() {
1102 let header =
1103 WwwAuthenticate::new("https://mcp.example.com/.well-known/oauth-protected-resource")
1104 .with_realm("mcp")
1105 .with_error(OAuthError::InvalidToken)
1106 .with_error_description("Token expired");
1107
1108 let value = header.to_header_value();
1109 assert!(value.starts_with("Bearer resource_metadata="));
1110 assert!(value.contains("realm=\"mcp\""));
1111 assert!(value.contains("error=\"invalid_token\""));
1112 }
1113
1114 #[test]
1115 fn test_www_authenticate_parse() {
1116 let header_value = "Bearer resource_metadata=\"https://example.com/.well-known/oauth-protected-resource\", realm=\"mcp\"";
1117 let parsed = WwwAuthenticate::parse(header_value);
1118
1119 assert!(parsed.is_some());
1120 let parsed = parsed.unwrap();
1121 assert_eq!(
1122 parsed.resource_metadata,
1123 "https://example.com/.well-known/oauth-protected-resource"
1124 );
1125 assert_eq!(parsed.realm, Some("mcp".to_string()));
1126 }
1127
1128 #[test]
1129 fn test_authorization_config() {
1130 let config = AuthorizationConfig::new("https://auth.example.com")
1131 .with_client_id("my-client")
1132 .with_resource("https://mcp.example.com")
1133 .with_scope("mcp:read");
1134
1135 assert!(config.is_public_client());
1136 assert_eq!(config.client_id, "my-client");
1137
1138 let config = config.with_client_secret("secret");
1139 assert!(!config.is_public_client());
1140 }
1141
1142 #[test]
1143 fn test_stored_token() {
1144 let token = TokenResponse {
1145 access_token: "access123".to_string(),
1146 token_type: "Bearer".to_string(),
1147 expires_in: Some(3600),
1148 refresh_token: Some("refresh456".to_string()),
1149 scope: Some("mcp:read".to_string()),
1150 };
1151
1152 let stored = StoredToken::new(token, "https://mcp.example.com");
1153 assert!(!stored.is_expired());
1154 assert_eq!(stored.authorization_header(), "Bearer access123");
1155 }
1156
1157 #[test]
1158 fn test_client_registration_request() {
1159 let request = ClientRegistrationRequest::new()
1160 .with_client_name("My MCP Client")
1161 .with_redirect_uris(["http://localhost:8080/callback"]);
1162
1163 assert_eq!(request.client_name, Some("My MCP Client".to_string()));
1164 assert!(request.redirect_uris.is_some());
1165 }
1166
1167 #[test]
1168 fn test_oauth_error_display() {
1169 assert_eq!(OAuthError::InvalidRequest.to_string(), "invalid_request");
1170 assert_eq!(OAuthError::InvalidToken.to_string(), "invalid_token");
1171 assert_eq!(
1172 OAuthError::InsufficientScope.to_string(),
1173 "insufficient_scope"
1174 );
1175 }
1176}