Skip to main content

mcpkit_core/auth/
oauth.rs

1//! OAuth 2.1 types and utilities.
2//!
3//! This submodule contains OAuth 2.1 protocol types including:
4//! - Protected Resource Metadata (RFC 9728)
5//! - Authorization Server Metadata (RFC 8414)
6//! - PKCE support (RFC 7636)
7//! - Dynamic Client Registration (RFC 7591)
8//! - Token request/response types
9
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::time::{Duration, SystemTime};
13
14/// OAuth 2.1 error types.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum OAuthError {
18    /// The request is missing a required parameter or is otherwise malformed.
19    InvalidRequest,
20    /// The client is not authorized to request an access token.
21    UnauthorizedClient,
22    /// The resource owner denied the request.
23    AccessDenied,
24    /// The authorization server does not support this response type.
25    UnsupportedResponseType,
26    /// The requested scope is invalid, unknown, or malformed.
27    InvalidScope,
28    /// The authorization server encountered an unexpected error.
29    ServerError,
30    /// The server is temporarily unavailable.
31    TemporarilyUnavailable,
32    /// The provided authorization grant is invalid or expired.
33    InvalidGrant,
34    /// The client authentication failed.
35    InvalidClient,
36    /// The grant type is not supported.
37    UnsupportedGrantType,
38    /// The token is invalid.
39    InvalidToken,
40    /// Insufficient scope for the requested resource.
41    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/// OAuth 2.1 error response.
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct OAuthErrorResponse {
68    /// The error code.
69    pub error: OAuthError,
70    /// Human-readable error description.
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub error_description: Option<String>,
73    /// URI for more information about the error.
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub error_uri: Option<String>,
76}
77
78impl OAuthErrorResponse {
79    /// Create a new error response.
80    #[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    /// Add a description to the error.
90    #[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/// Protected Resource Metadata per RFC 9728.
98///
99/// MCP servers MUST implement this to indicate the locations of
100/// authorization servers.
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct ProtectedResourceMetadata {
103    /// The protected resource identifier (URL).
104    pub resource: String,
105
106    /// List of authorization server URLs that can issue tokens for this resource.
107    /// MCP requires at least one authorization server.
108    pub authorization_servers: Vec<String>,
109
110    /// OAuth 2.0 Bearer token profile URL.
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub bearer_methods_supported: Option<Vec<String>>,
113
114    /// Resource documentation URL.
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub resource_documentation: Option<String>,
117
118    /// Supported scopes for this resource.
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub scopes_supported: Option<Vec<String>>,
121
122    /// JWS algorithms supported for resource signing.
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub resource_signing_alg_values_supported: Option<Vec<String>>,
125
126    /// Additional metadata fields.
127    #[serde(flatten)]
128    pub extra: HashMap<String, serde_json::Value>,
129}
130
131impl ProtectedResourceMetadata {
132    /// Create new protected resource metadata.
133    ///
134    /// # Arguments
135    ///
136    /// * `resource` - The protected resource identifier (URL of the MCP server)
137    #[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    /// Add an authorization server.
151    #[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    /// Set supported scopes.
158    #[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    /// Set documentation URL.
165    #[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    /// Validate that the metadata meets MCP requirements.
172    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    /// Get the well-known URL for this resource.
185    #[must_use]
186    pub fn well_known_url(resource_url: &str) -> Option<String> {
187        // Parse the URL and construct the well-known path
188        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/// Authorization Server Metadata per RFC 8414.
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct AuthorizationServerMetadata {
201    /// The authorization server's issuer identifier.
202    pub issuer: String,
203
204    /// URL of the authorization endpoint.
205    pub authorization_endpoint: String,
206
207    /// URL of the token endpoint.
208    pub token_endpoint: String,
209
210    /// URL of the JWKS endpoint.
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub jwks_uri: Option<String>,
213
214    /// URL of the dynamic client registration endpoint.
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub registration_endpoint: Option<String>,
217
218    /// Supported scopes.
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub scopes_supported: Option<Vec<String>>,
221
222    /// Supported response types.
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub response_types_supported: Option<Vec<String>>,
225
226    /// Supported grant types.
227    #[serde(skip_serializing_if = "Option::is_none")]
228    pub grant_types_supported: Option<Vec<String>>,
229
230    /// Supported token endpoint auth methods.
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub token_endpoint_auth_methods_supported: Option<Vec<String>>,
233
234    /// Supported PKCE code challenge methods.
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub code_challenge_methods_supported: Option<Vec<String>>,
237
238    /// URL of the revocation endpoint.
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub revocation_endpoint: Option<String>,
241
242    /// URL of the introspection endpoint.
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub introspection_endpoint: Option<String>,
245
246    /// Additional metadata fields.
247    #[serde(flatten)]
248    pub extra: HashMap<String, serde_json::Value>,
249}
250
251impl AuthorizationServerMetadata {
252    /// Create authorization server metadata with required fields.
253    #[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    /// Create metadata from an issuer URL using default paths.
281    #[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    /// Set the JWKS URI.
292    #[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    /// Set the registration endpoint.
299    #[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    /// Get the well-known URL for discovering this metadata.
306    #[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/// OAuth 2.1 grant types supported by MCP.
319#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
320#[serde(rename_all = "snake_case")]
321pub enum GrantType {
322    /// Authorization code grant (for user authorization).
323    AuthorizationCode,
324    /// Client credentials grant (for machine-to-machine).
325    ClientCredentials,
326    /// Refresh token grant.
327    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/// PKCE code challenge method.
341#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
342pub enum CodeChallengeMethod {
343    /// SHA-256 (recommended).
344    #[default]
345    S256,
346    /// Plain (not recommended, for legacy support only).
347    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/// PKCE code verifier and challenge.
360#[derive(Debug, Clone)]
361pub struct PkceChallenge {
362    /// The code verifier (random string).
363    pub verifier: String,
364    /// The code challenge (derived from verifier).
365    pub challenge: String,
366    /// The challenge method used.
367    pub method: CodeChallengeMethod,
368}
369
370impl PkceChallenge {
371    /// Generate a new PKCE challenge using S256.
372    #[must_use]
373    pub fn new() -> Self {
374        Self::with_method(CodeChallengeMethod::S256)
375    }
376
377    /// Generate a PKCE challenge with the specified method.
378    #[must_use]
379    pub fn with_method(method: CodeChallengeMethod) -> Self {
380        use base64::Engine;
381        use rand::Rng;
382
383        // Generate a random 32-byte verifier
384        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    /// Verify a code verifier against a challenge.
405    #[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 compare so the matching is not short-circuited on the
419        // first differing byte, which could leak the challenge via timing.
420        constant_time_eq(computed.as_bytes(), challenge.as_bytes())
421    }
422}
423
424/// Compare two byte slices without short-circuiting on the first mismatch.
425///
426/// Returns `false` immediately for differing lengths (the PKCE challenge length
427/// is not secret); for equal lengths the time taken does not depend on where
428/// the first differing byte is.
429fn 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/// Authorization request parameters.
447#[derive(Debug, Clone, Serialize, Deserialize)]
448pub struct AuthorizationRequest {
449    /// The response type (must be "code" for authorization code flow).
450    pub response_type: String,
451    /// The client identifier.
452    pub client_id: String,
453    /// The redirect URI.
454    #[serde(skip_serializing_if = "Option::is_none")]
455    pub redirect_uri: Option<String>,
456    /// The requested scope.
457    #[serde(skip_serializing_if = "Option::is_none")]
458    pub scope: Option<String>,
459    /// State parameter for CSRF protection.
460    #[serde(skip_serializing_if = "Option::is_none")]
461    pub state: Option<String>,
462    /// PKCE code challenge.
463    pub code_challenge: String,
464    /// PKCE code challenge method.
465    pub code_challenge_method: String,
466    /// Resource indicator (RFC 8707) - REQUIRED by MCP.
467    pub resource: String,
468}
469
470impl AuthorizationRequest {
471    /// Create a new authorization request with PKCE.
472    #[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    /// Set the redirect URI.
491    #[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    /// Set the requested scope.
498    #[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    /// Set the state parameter.
505    #[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    /// Build the authorization URL.
512    #[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/// Token request for authorization code exchange.
540#[derive(Debug, Clone, Serialize, Deserialize)]
541pub struct TokenRequest {
542    /// The grant type.
543    pub grant_type: String,
544    /// The authorization code (for `authorization_code` grant).
545    #[serde(skip_serializing_if = "Option::is_none")]
546    pub code: Option<String>,
547    /// The redirect URI (must match the one in authorization request).
548    #[serde(skip_serializing_if = "Option::is_none")]
549    pub redirect_uri: Option<String>,
550    /// The client identifier.
551    pub client_id: String,
552    /// The client secret (for confidential clients).
553    #[serde(skip_serializing_if = "Option::is_none")]
554    pub client_secret: Option<String>,
555    /// PKCE code verifier.
556    #[serde(skip_serializing_if = "Option::is_none")]
557    pub code_verifier: Option<String>,
558    /// Resource indicator (RFC 8707).
559    #[serde(skip_serializing_if = "Option::is_none")]
560    pub resource: Option<String>,
561    /// Refresh token (for `refresh_token` grant).
562    #[serde(skip_serializing_if = "Option::is_none")]
563    pub refresh_token: Option<String>,
564    /// Requested scope.
565    #[serde(skip_serializing_if = "Option::is_none")]
566    pub scope: Option<String>,
567}
568
569impl TokenRequest {
570    /// Create a token request for authorization code exchange.
571    #[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    /// Create a token request for client credentials grant.
592    #[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    /// Create a token request for refresh token grant.
612    #[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    /// Set the redirect URI.
628    #[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    /// Set the requested scope.
635    #[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/// Token response from the authorization server.
643#[derive(Debug, Clone, Serialize, Deserialize)]
644pub struct TokenResponse {
645    /// The access token.
646    pub access_token: String,
647    /// The token type (typically "Bearer").
648    pub token_type: String,
649    /// The token lifetime in seconds.
650    #[serde(skip_serializing_if = "Option::is_none")]
651    pub expires_in: Option<u64>,
652    /// The refresh token.
653    #[serde(skip_serializing_if = "Option::is_none")]
654    pub refresh_token: Option<String>,
655    /// The granted scope.
656    #[serde(skip_serializing_if = "Option::is_none")]
657    pub scope: Option<String>,
658}
659
660impl TokenResponse {
661    /// Check if the token is expired.
662    #[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 // No expiration means never expires
669        }
670    }
671}
672
673/// Access token with metadata for client-side storage.
674#[derive(Debug, Clone)]
675pub struct StoredToken {
676    /// The token response.
677    pub token: TokenResponse,
678    /// When the token was issued.
679    pub issued_at: SystemTime,
680    /// The resource this token is bound to.
681    pub resource: String,
682}
683
684impl StoredToken {
685    /// Create a new stored token.
686    #[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    /// Check if the token is expired.
696    #[must_use]
697    pub fn is_expired(&self) -> bool {
698        self.token.is_expired(self.issued_at)
699    }
700
701    /// Check if the token will expire within the given duration.
702    #[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    /// Get the Authorization header value.
713    #[must_use]
714    pub fn authorization_header(&self) -> String {
715        format!("Bearer {}", self.token.access_token)
716    }
717}
718
719/// WWW-Authenticate header builder for 401 responses.
720#[derive(Debug, Clone)]
721pub struct WwwAuthenticate {
722    /// The authentication realm.
723    pub realm: Option<String>,
724    /// The resource metadata URL.
725    pub resource_metadata: String,
726    /// The error code.
727    pub error: Option<OAuthError>,
728    /// The error description.
729    pub error_description: Option<String>,
730}
731
732impl WwwAuthenticate {
733    /// Create a new WWW-Authenticate header.
734    #[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    /// Set the realm.
745    #[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    /// Set the error.
752    #[must_use]
753    pub const fn with_error(mut self, error: OAuthError) -> Self {
754        self.error = Some(error);
755        self
756    }
757
758    /// Set the error description.
759    #[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    /// Build the header value string per RFC 9728.
766    #[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    /// Parse a WWW-Authenticate header value.
787    #[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..]; // Skip "Bearer "
794        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/// Client authorization configuration.
829#[derive(Debug, Clone)]
830pub struct AuthorizationConfig {
831    /// The authorization server URL.
832    pub authorization_server: String,
833    /// The client identifier.
834    pub client_id: String,
835    /// The client secret (for confidential clients).
836    pub client_secret: Option<String>,
837    /// The redirect URI for authorization code flow.
838    pub redirect_uri: Option<String>,
839    /// The target resource (MCP server URL).
840    pub resource: Option<String>,
841    /// The requested scopes.
842    pub scopes: Vec<String>,
843}
844
845impl AuthorizationConfig {
846    /// Create a new authorization configuration.
847    #[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    /// Set the client ID.
860    #[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    /// Set the client secret.
867    #[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    /// Set the redirect URI.
874    #[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    /// Set the target resource (MCP server URL).
881    #[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    /// Add a scope.
888    #[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    /// Check if this is a public client (no client secret).
895    #[must_use]
896    pub const fn is_public_client(&self) -> bool {
897        self.client_secret.is_none()
898    }
899}
900
901/// Dynamic Client Registration request per RFC 7591.
902#[derive(Debug, Clone, Serialize, Deserialize)]
903pub struct ClientRegistrationRequest {
904    /// Requested redirect URIs.
905    #[serde(skip_serializing_if = "Option::is_none")]
906    pub redirect_uris: Option<Vec<String>>,
907    /// Token endpoint authentication method.
908    #[serde(skip_serializing_if = "Option::is_none")]
909    pub token_endpoint_auth_method: Option<String>,
910    /// Requested grant types.
911    #[serde(skip_serializing_if = "Option::is_none")]
912    pub grant_types: Option<Vec<String>>,
913    /// Requested response types.
914    #[serde(skip_serializing_if = "Option::is_none")]
915    pub response_types: Option<Vec<String>>,
916    /// Client name.
917    #[serde(skip_serializing_if = "Option::is_none")]
918    pub client_name: Option<String>,
919    /// Client URI.
920    #[serde(skip_serializing_if = "Option::is_none")]
921    pub client_uri: Option<String>,
922    /// Software identifier.
923    #[serde(skip_serializing_if = "Option::is_none")]
924    pub software_id: Option<String>,
925    /// Software version.
926    #[serde(skip_serializing_if = "Option::is_none")]
927    pub software_version: Option<String>,
928}
929
930impl ClientRegistrationRequest {
931    /// Create a new client registration request.
932    #[must_use]
933    pub fn new() -> Self {
934        Self {
935            redirect_uris: None,
936            token_endpoint_auth_method: Some("none".to_string()), // Public client
937            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    /// Set redirect URIs.
947    #[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    /// Set the client name.
954    #[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    /// Set the software ID.
961    #[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/// Dynamic Client Registration response per RFC 7591.
975#[derive(Debug, Clone, Serialize, Deserialize)]
976pub struct ClientRegistrationResponse {
977    /// The assigned client identifier.
978    pub client_id: String,
979    /// The client secret (if applicable).
980    #[serde(skip_serializing_if = "Option::is_none")]
981    pub client_secret: Option<String>,
982    /// Client secret expiration time.
983    #[serde(skip_serializing_if = "Option::is_none")]
984    pub client_secret_expires_at: Option<u64>,
985    /// Client ID issued at time.
986    #[serde(skip_serializing_if = "Option::is_none")]
987    pub client_id_issued_at: Option<u64>,
988    /// All other registered metadata echoed back.
989    #[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()); // No auth servers
1012
1013        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        // Verifier should be different from challenge for S256
1034        assert_ne!(pkce.verifier, pkce.challenge);
1035
1036        // Verification should work
1037        assert!(PkceChallenge::verify(
1038            &pkce.verifier,
1039            &pkce.challenge,
1040            CodeChallengeMethod::S256
1041        ));
1042
1043        // Wrong verifier should fail
1044        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}