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