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(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(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(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(Clone)]
830#[non_exhaustive]
831pub struct AuthorizationConfig {
832    /// The authorization server URL.
833    pub authorization_server: String,
834    /// The client identifier.
835    pub client_id: String,
836    /// The client secret (for confidential clients).
837    pub client_secret: Option<String>,
838    /// The redirect URI for authorization code flow.
839    pub redirect_uri: Option<String>,
840    /// The target resource (MCP server URL).
841    pub resource: Option<String>,
842    /// The requested scopes.
843    pub scopes: Vec<String>,
844}
845
846impl AuthorizationConfig {
847    /// Create a new authorization configuration.
848    #[must_use]
849    pub fn new(authorization_server: impl Into<String>) -> Self {
850        Self {
851            authorization_server: authorization_server.into(),
852            client_id: String::new(),
853            client_secret: None,
854            redirect_uri: None,
855            resource: None,
856            scopes: Vec::new(),
857        }
858    }
859
860    /// Set the client ID.
861    #[must_use]
862    pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
863        self.client_id = client_id.into();
864        self
865    }
866
867    /// Set the client secret.
868    #[must_use]
869    pub fn with_client_secret(mut self, secret: impl Into<String>) -> Self {
870        self.client_secret = Some(secret.into());
871        self
872    }
873
874    /// Set the redirect URI.
875    #[must_use]
876    pub fn with_redirect_uri(mut self, uri: impl Into<String>) -> Self {
877        self.redirect_uri = Some(uri.into());
878        self
879    }
880
881    /// Set the target resource (MCP server URL).
882    #[must_use]
883    pub fn with_resource(mut self, resource: impl Into<String>) -> Self {
884        self.resource = Some(resource.into());
885        self
886    }
887
888    /// Add a scope.
889    #[must_use]
890    pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
891        self.scopes.push(scope.into());
892        self
893    }
894
895    /// Check if this is a public client (no client secret).
896    #[must_use]
897    pub const fn is_public_client(&self) -> bool {
898        self.client_secret.is_none()
899    }
900}
901
902/// Dynamic Client Registration request per RFC 7591.
903#[derive(Debug, Clone, Serialize, Deserialize)]
904pub struct ClientRegistrationRequest {
905    /// Requested redirect URIs.
906    #[serde(skip_serializing_if = "Option::is_none")]
907    pub redirect_uris: Option<Vec<String>>,
908    /// Token endpoint authentication method.
909    #[serde(skip_serializing_if = "Option::is_none")]
910    pub token_endpoint_auth_method: Option<String>,
911    /// Requested grant types.
912    #[serde(skip_serializing_if = "Option::is_none")]
913    pub grant_types: Option<Vec<String>>,
914    /// Requested response types.
915    #[serde(skip_serializing_if = "Option::is_none")]
916    pub response_types: Option<Vec<String>>,
917    /// Client name.
918    #[serde(skip_serializing_if = "Option::is_none")]
919    pub client_name: Option<String>,
920    /// Client URI.
921    #[serde(skip_serializing_if = "Option::is_none")]
922    pub client_uri: Option<String>,
923    /// Software identifier.
924    #[serde(skip_serializing_if = "Option::is_none")]
925    pub software_id: Option<String>,
926    /// Software version.
927    #[serde(skip_serializing_if = "Option::is_none")]
928    pub software_version: Option<String>,
929}
930
931impl ClientRegistrationRequest {
932    /// Create a new client registration request.
933    #[must_use]
934    pub fn new() -> Self {
935        Self {
936            redirect_uris: None,
937            token_endpoint_auth_method: Some("none".to_string()), // Public client
938            grant_types: Some(vec!["authorization_code".to_string()]),
939            response_types: Some(vec!["code".to_string()]),
940            client_name: None,
941            client_uri: None,
942            software_id: None,
943            software_version: None,
944        }
945    }
946
947    /// Set redirect URIs.
948    #[must_use]
949    pub fn with_redirect_uris(mut self, uris: impl IntoIterator<Item = impl Into<String>>) -> Self {
950        self.redirect_uris = Some(uris.into_iter().map(Into::into).collect());
951        self
952    }
953
954    /// Set the client name.
955    #[must_use]
956    pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
957        self.client_name = Some(name.into());
958        self
959    }
960
961    /// Set the software ID.
962    #[must_use]
963    pub fn with_software_id(mut self, id: impl Into<String>) -> Self {
964        self.software_id = Some(id.into());
965        self
966    }
967}
968
969impl Default for ClientRegistrationRequest {
970    fn default() -> Self {
971        Self::new()
972    }
973}
974
975/// Dynamic Client Registration response per RFC 7591.
976#[derive(Clone, Serialize, Deserialize)]
977pub struct ClientRegistrationResponse {
978    /// The assigned client identifier.
979    pub client_id: String,
980    /// The client secret (if applicable).
981    #[serde(skip_serializing_if = "Option::is_none")]
982    pub client_secret: Option<String>,
983    /// Client secret expiration time.
984    #[serde(skip_serializing_if = "Option::is_none")]
985    pub client_secret_expires_at: Option<u64>,
986    /// Client ID issued at time.
987    #[serde(skip_serializing_if = "Option::is_none")]
988    pub client_id_issued_at: Option<u64>,
989    /// All other registered metadata echoed back.
990    #[serde(flatten)]
991    pub metadata: HashMap<String, serde_json::Value>,
992}
993
994// Redacted `Debug` impls for the secret-bearing OAuth types. Deriving `Debug`
995// would write bearer tokens, client secrets, authorization codes, and PKCE
996// verifiers verbatim into logs/traces; these redact the secret fields while
997// preserving presence (`Some`/`None`) for diagnostics.
998
999impl std::fmt::Debug for TokenResponse {
1000    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1001        f.debug_struct("TokenResponse")
1002            .field("access_token", &"<redacted>")
1003            .field("token_type", &self.token_type)
1004            .field("expires_in", &self.expires_in)
1005            .field(
1006                "refresh_token",
1007                &self.refresh_token.as_ref().map(|_| "<redacted>"),
1008            )
1009            .field("scope", &self.scope)
1010            .finish()
1011    }
1012}
1013
1014impl std::fmt::Debug for TokenRequest {
1015    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1016        f.debug_struct("TokenRequest")
1017            .field("grant_type", &self.grant_type)
1018            .field("code", &self.code.as_ref().map(|_| "<redacted>"))
1019            .field("redirect_uri", &self.redirect_uri)
1020            .field("client_id", &self.client_id)
1021            .field(
1022                "client_secret",
1023                &self.client_secret.as_ref().map(|_| "<redacted>"),
1024            )
1025            .field(
1026                "code_verifier",
1027                &self.code_verifier.as_ref().map(|_| "<redacted>"),
1028            )
1029            .field("resource", &self.resource)
1030            .field(
1031                "refresh_token",
1032                &self.refresh_token.as_ref().map(|_| "<redacted>"),
1033            )
1034            .field("scope", &self.scope)
1035            .finish()
1036    }
1037}
1038
1039impl std::fmt::Debug for AuthorizationConfig {
1040    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1041        f.debug_struct("AuthorizationConfig")
1042            .field("authorization_server", &self.authorization_server)
1043            .field("client_id", &self.client_id)
1044            .field(
1045                "client_secret",
1046                &self.client_secret.as_ref().map(|_| "<redacted>"),
1047            )
1048            .field("redirect_uri", &self.redirect_uri)
1049            .field("resource", &self.resource)
1050            .field("scopes", &self.scopes)
1051            .finish()
1052    }
1053}
1054
1055impl std::fmt::Debug for PkceChallenge {
1056    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1057        f.debug_struct("PkceChallenge")
1058            .field("verifier", &"<redacted>")
1059            .field("challenge", &self.challenge)
1060            .field("method", &self.method)
1061            .finish()
1062    }
1063}
1064
1065impl std::fmt::Debug for ClientRegistrationResponse {
1066    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1067        f.debug_struct("ClientRegistrationResponse")
1068            .field("client_id", &self.client_id)
1069            .field(
1070                "client_secret",
1071                &self.client_secret.as_ref().map(|_| "<redacted>"),
1072            )
1073            .field("client_secret_expires_at", &self.client_secret_expires_at)
1074            .field("client_id_issued_at", &self.client_id_issued_at)
1075            .field("metadata", &self.metadata)
1076            .finish()
1077    }
1078}
1079
1080#[cfg(test)]
1081mod tests {
1082    use super::*;
1083
1084    #[test]
1085    fn test_secret_bearing_debug_is_redacted() {
1086        let token = TokenResponse {
1087            access_token: "super-secret-access".to_string(),
1088            token_type: "Bearer".to_string(),
1089            expires_in: Some(3600),
1090            refresh_token: Some("super-secret-refresh".to_string()),
1091            scope: Some("mcp:read".to_string()),
1092        };
1093        let dbg = format!("{token:?}");
1094        assert!(
1095            !dbg.contains("super-secret-access"),
1096            "access_token leaked: {dbg}"
1097        );
1098        assert!(
1099            !dbg.contains("super-secret-refresh"),
1100            "refresh_token leaked: {dbg}"
1101        );
1102        assert!(dbg.contains("<redacted>"));
1103        // Non-secret metadata is still visible.
1104        assert!(dbg.contains("Bearer"));
1105
1106        let pkce = PkceChallenge::new();
1107        let pkce_dbg = format!("{pkce:?}");
1108        assert!(
1109            !pkce_dbg.contains(&pkce.verifier),
1110            "PKCE verifier leaked: {pkce_dbg}"
1111        );
1112
1113        let cfg = AuthorizationConfig::new("https://auth.example.com");
1114        let cfg2 = AuthorizationConfig {
1115            client_secret: Some("super-secret-client".to_string()),
1116            ..cfg
1117        };
1118        assert!(!format!("{cfg2:?}").contains("super-secret-client"));
1119    }
1120
1121    #[test]
1122    fn test_protected_resource_metadata() {
1123        let metadata = ProtectedResourceMetadata::new("https://mcp.example.com")
1124            .with_authorization_server("https://auth.example.com")
1125            .with_scopes(["mcp:read", "mcp:write"]);
1126
1127        assert_eq!(metadata.resource, "https://mcp.example.com");
1128        assert_eq!(metadata.authorization_servers.len(), 1);
1129        assert!(metadata.validate().is_ok());
1130    }
1131
1132    #[test]
1133    fn test_protected_resource_metadata_validation() {
1134        let metadata = ProtectedResourceMetadata::new("https://mcp.example.com");
1135        assert!(metadata.validate().is_err()); // No auth servers
1136
1137        let metadata = metadata.with_authorization_server("https://auth.example.com");
1138        assert!(metadata.validate().is_ok());
1139    }
1140
1141    #[test]
1142    fn test_authorization_server_metadata() {
1143        let metadata = AuthorizationServerMetadata::from_issuer("https://auth.example.com");
1144
1145        assert_eq!(metadata.issuer, "https://auth.example.com");
1146        assert_eq!(
1147            metadata.authorization_endpoint,
1148            "https://auth.example.com/authorize"
1149        );
1150        assert_eq!(metadata.token_endpoint, "https://auth.example.com/token");
1151    }
1152
1153    #[test]
1154    fn test_pkce_challenge() {
1155        let pkce = PkceChallenge::new();
1156
1157        // Verifier should be different from challenge for S256
1158        assert_ne!(pkce.verifier, pkce.challenge);
1159
1160        // Verification should work
1161        assert!(PkceChallenge::verify(
1162            &pkce.verifier,
1163            &pkce.challenge,
1164            CodeChallengeMethod::S256
1165        ));
1166
1167        // Wrong verifier should fail
1168        assert!(!PkceChallenge::verify(
1169            "wrong",
1170            &pkce.challenge,
1171            CodeChallengeMethod::S256
1172        ));
1173    }
1174
1175    #[test]
1176    fn test_constant_time_eq() {
1177        assert!(constant_time_eq(b"abc", b"abc"));
1178        assert!(constant_time_eq(b"", b""));
1179        assert!(!constant_time_eq(b"abc", b"abd"));
1180        assert!(!constant_time_eq(b"abc", b"abcd"));
1181        assert!(!constant_time_eq(b"abc", b""));
1182    }
1183
1184    #[test]
1185    fn test_authorization_request() -> Result<(), Box<dyn std::error::Error>> {
1186        let pkce = PkceChallenge::new();
1187        let request = AuthorizationRequest::new("client123", &pkce, "https://mcp.example.com")
1188            .with_redirect_uri("http://localhost:8080/callback")
1189            .with_scope("mcp:read")
1190            .with_state("random_state");
1191
1192        assert_eq!(request.client_id, "client123");
1193        assert_eq!(request.resource, "https://mcp.example.com");
1194
1195        let url = request.build_url("https://auth.example.com/authorize");
1196        assert!(url.is_some());
1197        let url = url.ok_or("Expected URL")?;
1198        assert!(url.contains("response_type=code"));
1199        assert!(url.contains("client_id=client123"));
1200        assert!(url.contains("resource="));
1201        Ok(())
1202    }
1203
1204    #[test]
1205    fn test_token_request_authorization_code() {
1206        let request = TokenRequest::authorization_code(
1207            "auth_code_123",
1208            "client123",
1209            "verifier123",
1210            "https://mcp.example.com",
1211        );
1212
1213        assert_eq!(request.grant_type, "authorization_code");
1214        assert_eq!(request.code, Some("auth_code_123".to_string()));
1215        assert_eq!(request.code_verifier, Some("verifier123".to_string()));
1216    }
1217
1218    #[test]
1219    fn test_token_request_client_credentials() {
1220        let request =
1221            TokenRequest::client_credentials("client123", "secret456", "https://mcp.example.com");
1222
1223        assert_eq!(request.grant_type, "client_credentials");
1224        assert_eq!(request.client_secret, Some("secret456".to_string()));
1225    }
1226
1227    #[test]
1228    fn test_www_authenticate_header() {
1229        let header =
1230            WwwAuthenticate::new("https://mcp.example.com/.well-known/oauth-protected-resource")
1231                .with_realm("mcp")
1232                .with_error(OAuthError::InvalidToken)
1233                .with_error_description("Token expired");
1234
1235        let value = header.to_header_value();
1236        assert!(value.starts_with("Bearer resource_metadata="));
1237        assert!(value.contains("realm=\"mcp\""));
1238        assert!(value.contains("error=\"invalid_token\""));
1239    }
1240
1241    #[test]
1242    fn test_www_authenticate_parse() -> Result<(), Box<dyn std::error::Error>> {
1243        let header_value = "Bearer resource_metadata=\"https://example.com/.well-known/oauth-protected-resource\", realm=\"mcp\"";
1244        let parsed = WwwAuthenticate::parse(header_value);
1245
1246        assert!(parsed.is_some());
1247        let parsed = parsed.ok_or("Expected parsed header")?;
1248        assert_eq!(
1249            parsed.resource_metadata,
1250            "https://example.com/.well-known/oauth-protected-resource"
1251        );
1252        assert_eq!(parsed.realm, Some("mcp".to_string()));
1253        Ok(())
1254    }
1255
1256    #[test]
1257    fn test_authorization_config() {
1258        let config = AuthorizationConfig::new("https://auth.example.com")
1259            .with_client_id("my-client")
1260            .with_resource("https://mcp.example.com")
1261            .with_scope("mcp:read");
1262
1263        assert!(config.is_public_client());
1264        assert_eq!(config.client_id, "my-client");
1265
1266        let config = config.with_client_secret("secret");
1267        assert!(!config.is_public_client());
1268    }
1269
1270    #[test]
1271    fn test_stored_token() {
1272        let token = TokenResponse {
1273            access_token: "access123".to_string(),
1274            token_type: "Bearer".to_string(),
1275            expires_in: Some(3600),
1276            refresh_token: Some("refresh456".to_string()),
1277            scope: Some("mcp:read".to_string()),
1278        };
1279
1280        let stored = StoredToken::new(token, "https://mcp.example.com");
1281        assert!(!stored.is_expired());
1282        assert_eq!(stored.authorization_header(), "Bearer access123");
1283    }
1284
1285    #[test]
1286    fn test_client_registration_request() {
1287        let request = ClientRegistrationRequest::new()
1288            .with_client_name("My MCP Client")
1289            .with_redirect_uris(["http://localhost:8080/callback"]);
1290
1291        assert_eq!(request.client_name, Some("My MCP Client".to_string()));
1292        assert!(request.redirect_uris.is_some());
1293    }
1294
1295    #[test]
1296    fn test_oauth_error_display() {
1297        assert_eq!(OAuthError::InvalidRequest.to_string(), "invalid_request");
1298        assert_eq!(OAuthError::InvalidToken.to_string(), "invalid_token");
1299        assert_eq!(
1300            OAuthError::InsufficientScope.to_string(),
1301            "insufficient_scope"
1302        );
1303    }
1304}