mcpkit_core/
auth.rs

1//! OAuth 2.1 Authorization for MCP.
2//!
3//! This module implements OAuth 2.1 authorization per the MCP specification
4//! (2025-06-18), including:
5//!
6//! - Protected Resource Metadata (RFC 9728)
7//! - Resource Indicators (RFC 8707)
8//! - Authorization Server Metadata (RFC 8414)
9//! - PKCE (RFC 7636)
10//! - Dynamic Client Registration (RFC 7591)
11//!
12//! # Overview
13//!
14//! MCP authorization follows OAuth 2.1 with MCP servers acting as OAuth
15//! Resource Servers that validate tokens issued by external Authorization
16//! Servers.
17//!
18//! # Example
19//!
20//! ```rust
21//! use mcpkit_core::auth::{ProtectedResourceMetadata, AuthorizationConfig};
22//!
23//! // Server-side: Expose protected resource metadata
24//! let metadata = ProtectedResourceMetadata::new("https://mcp.example.com")
25//!     .with_authorization_server("https://auth.example.com");
26//! assert!(metadata.validate().is_ok());
27//!
28//! // Client-side: Configure authorization
29//! let config = AuthorizationConfig::new("https://auth.example.com")
30//!     .with_client_id("my-client")
31//!     .with_resource("https://mcp.example.com");
32//! assert_eq!(config.client_id, "my-client");
33//! ```
34
35use serde::{Deserialize, Serialize};
36use std::collections::HashMap;
37use std::time::{Duration, SystemTime};
38
39/// OAuth 2.1 error types.
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum OAuthError {
43    /// The request is missing a required parameter or is otherwise malformed.
44    InvalidRequest,
45    /// The client is not authorized to request an access token.
46    UnauthorizedClient,
47    /// The resource owner denied the request.
48    AccessDenied,
49    /// The authorization server does not support this response type.
50    UnsupportedResponseType,
51    /// The requested scope is invalid, unknown, or malformed.
52    InvalidScope,
53    /// The authorization server encountered an unexpected error.
54    ServerError,
55    /// The server is temporarily unavailable.
56    TemporarilyUnavailable,
57    /// The provided authorization grant is invalid or expired.
58    InvalidGrant,
59    /// The client authentication failed.
60    InvalidClient,
61    /// The grant type is not supported.
62    UnsupportedGrantType,
63    /// The token is invalid.
64    InvalidToken,
65    /// Insufficient scope for the requested resource.
66    InsufficientScope,
67}
68
69impl std::fmt::Display for OAuthError {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        match self {
72            Self::InvalidRequest => write!(f, "invalid_request"),
73            Self::UnauthorizedClient => write!(f, "unauthorized_client"),
74            Self::AccessDenied => write!(f, "access_denied"),
75            Self::UnsupportedResponseType => write!(f, "unsupported_response_type"),
76            Self::InvalidScope => write!(f, "invalid_scope"),
77            Self::ServerError => write!(f, "server_error"),
78            Self::TemporarilyUnavailable => write!(f, "temporarily_unavailable"),
79            Self::InvalidGrant => write!(f, "invalid_grant"),
80            Self::InvalidClient => write!(f, "invalid_client"),
81            Self::UnsupportedGrantType => write!(f, "unsupported_grant_type"),
82            Self::InvalidToken => write!(f, "invalid_token"),
83            Self::InsufficientScope => write!(f, "insufficient_scope"),
84        }
85    }
86}
87
88impl std::error::Error for OAuthError {}
89
90/// OAuth 2.1 error response.
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct OAuthErrorResponse {
93    /// The error code.
94    pub error: OAuthError,
95    /// Human-readable error description.
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub error_description: Option<String>,
98    /// URI for more information about the error.
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub error_uri: Option<String>,
101}
102
103impl OAuthErrorResponse {
104    /// Create a new error response.
105    #[must_use]
106    pub const fn new(error: OAuthError) -> Self {
107        Self {
108            error,
109            error_description: None,
110            error_uri: None,
111        }
112    }
113
114    /// Add a description to the error.
115    #[must_use]
116    pub fn with_description(mut self, description: impl Into<String>) -> Self {
117        self.error_description = Some(description.into());
118        self
119    }
120}
121
122/// Protected Resource Metadata per RFC 9728.
123///
124/// MCP servers MUST implement this to indicate the locations of
125/// authorization servers.
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct ProtectedResourceMetadata {
128    /// The protected resource identifier (URL).
129    pub resource: String,
130
131    /// List of authorization server URLs that can issue tokens for this resource.
132    /// MCP requires at least one authorization server.
133    pub authorization_servers: Vec<String>,
134
135    /// OAuth 2.0 Bearer token profile URL.
136    #[serde(skip_serializing_if = "Option::is_none")]
137    pub bearer_methods_supported: Option<Vec<String>>,
138
139    /// Resource documentation URL.
140    #[serde(skip_serializing_if = "Option::is_none")]
141    pub resource_documentation: Option<String>,
142
143    /// Supported scopes for this resource.
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub scopes_supported: Option<Vec<String>>,
146
147    /// JWS algorithms supported for resource signing.
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub resource_signing_alg_values_supported: Option<Vec<String>>,
150
151    /// Additional metadata fields.
152    #[serde(flatten)]
153    pub extra: HashMap<String, serde_json::Value>,
154}
155
156impl ProtectedResourceMetadata {
157    /// Create new protected resource metadata.
158    ///
159    /// # Arguments
160    ///
161    /// * `resource` - The protected resource identifier (URL of the MCP server)
162    #[must_use]
163    pub fn new(resource: impl Into<String>) -> Self {
164        Self {
165            resource: resource.into(),
166            authorization_servers: Vec::new(),
167            bearer_methods_supported: Some(vec!["header".to_string()]),
168            resource_documentation: None,
169            scopes_supported: None,
170            resource_signing_alg_values_supported: None,
171            extra: HashMap::new(),
172        }
173    }
174
175    /// Add an authorization server.
176    #[must_use]
177    pub fn with_authorization_server(mut self, server: impl Into<String>) -> Self {
178        self.authorization_servers.push(server.into());
179        self
180    }
181
182    /// Set supported scopes.
183    #[must_use]
184    pub fn with_scopes(mut self, scopes: impl IntoIterator<Item = impl Into<String>>) -> Self {
185        self.scopes_supported = Some(scopes.into_iter().map(Into::into).collect());
186        self
187    }
188
189    /// Set documentation URL.
190    #[must_use]
191    pub fn with_documentation(mut self, url: impl Into<String>) -> Self {
192        self.resource_documentation = Some(url.into());
193        self
194    }
195
196    /// Validate that the metadata meets MCP requirements.
197    pub fn validate(&self) -> Result<(), String> {
198        if self.resource.is_empty() {
199            return Err("Resource identifier is required".to_string());
200        }
201        if self.authorization_servers.is_empty() {
202            return Err(
203                "At least one authorization server is required per MCP specification".to_string(),
204            );
205        }
206        Ok(())
207    }
208
209    /// Get the well-known URL for this resource.
210    #[must_use]
211    pub fn well_known_url(resource_url: &str) -> Option<String> {
212        // Parse the URL and construct the well-known path
213        url::Url::parse(resource_url).ok().map(|url| {
214            format!(
215                "{}://{}/.well-known/oauth-protected-resource",
216                url.scheme(),
217                url.host_str().unwrap_or("localhost")
218            )
219        })
220    }
221}
222
223/// Authorization Server Metadata per RFC 8414.
224#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct AuthorizationServerMetadata {
226    /// The authorization server's issuer identifier.
227    pub issuer: String,
228
229    /// URL of the authorization endpoint.
230    pub authorization_endpoint: String,
231
232    /// URL of the token endpoint.
233    pub token_endpoint: String,
234
235    /// URL of the JWKS endpoint.
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub jwks_uri: Option<String>,
238
239    /// URL of the dynamic client registration endpoint.
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub registration_endpoint: Option<String>,
242
243    /// Supported scopes.
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub scopes_supported: Option<Vec<String>>,
246
247    /// Supported response types.
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub response_types_supported: Option<Vec<String>>,
250
251    /// Supported grant types.
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub grant_types_supported: Option<Vec<String>>,
254
255    /// Supported token endpoint auth methods.
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub token_endpoint_auth_methods_supported: Option<Vec<String>>,
258
259    /// Supported PKCE code challenge methods.
260    #[serde(skip_serializing_if = "Option::is_none")]
261    pub code_challenge_methods_supported: Option<Vec<String>>,
262
263    /// URL of the revocation endpoint.
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub revocation_endpoint: Option<String>,
266
267    /// URL of the introspection endpoint.
268    #[serde(skip_serializing_if = "Option::is_none")]
269    pub introspection_endpoint: Option<String>,
270
271    /// Additional metadata fields.
272    #[serde(flatten)]
273    pub extra: HashMap<String, serde_json::Value>,
274}
275
276impl AuthorizationServerMetadata {
277    /// Create authorization server metadata with required fields.
278    #[must_use]
279    pub fn new(
280        issuer: impl Into<String>,
281        authorization_endpoint: impl Into<String>,
282        token_endpoint: impl Into<String>,
283    ) -> Self {
284        Self {
285            issuer: issuer.into(),
286            authorization_endpoint: authorization_endpoint.into(),
287            token_endpoint: token_endpoint.into(),
288            jwks_uri: None,
289            registration_endpoint: None,
290            scopes_supported: None,
291            response_types_supported: Some(vec!["code".to_string()]),
292            grant_types_supported: Some(vec![
293                "authorization_code".to_string(),
294                "client_credentials".to_string(),
295                "refresh_token".to_string(),
296            ]),
297            token_endpoint_auth_methods_supported: None,
298            code_challenge_methods_supported: Some(vec!["S256".to_string()]),
299            revocation_endpoint: None,
300            introspection_endpoint: None,
301            extra: HashMap::new(),
302        }
303    }
304
305    /// Create metadata from an issuer URL using default paths.
306    #[must_use]
307    pub fn from_issuer(issuer: impl Into<String>) -> Self {
308        let issuer = issuer.into();
309        Self::new(
310            &issuer,
311            format!("{issuer}/authorize"),
312            format!("{issuer}/token"),
313        )
314    }
315
316    /// Set the JWKS URI.
317    #[must_use]
318    pub fn with_jwks_uri(mut self, uri: impl Into<String>) -> Self {
319        self.jwks_uri = Some(uri.into());
320        self
321    }
322
323    /// Set the registration endpoint.
324    #[must_use]
325    pub fn with_registration_endpoint(mut self, endpoint: impl Into<String>) -> Self {
326        self.registration_endpoint = Some(endpoint.into());
327        self
328    }
329
330    /// Get the well-known URL for discovering this metadata.
331    #[must_use]
332    pub fn well_known_url(issuer: &str) -> Option<String> {
333        url::Url::parse(issuer).ok().map(|url| {
334            format!(
335                "{}://{}/.well-known/oauth-authorization-server",
336                url.scheme(),
337                url.host_str().unwrap_or("localhost")
338            )
339        })
340    }
341}
342
343/// OAuth 2.1 grant types supported by MCP.
344#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
345#[serde(rename_all = "snake_case")]
346pub enum GrantType {
347    /// Authorization code grant (for user authorization).
348    AuthorizationCode,
349    /// Client credentials grant (for machine-to-machine).
350    ClientCredentials,
351    /// Refresh token grant.
352    RefreshToken,
353}
354
355impl std::fmt::Display for GrantType {
356    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
357        match self {
358            Self::AuthorizationCode => write!(f, "authorization_code"),
359            Self::ClientCredentials => write!(f, "client_credentials"),
360            Self::RefreshToken => write!(f, "refresh_token"),
361        }
362    }
363}
364
365/// PKCE code challenge method.
366#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
367pub enum CodeChallengeMethod {
368    /// SHA-256 (recommended).
369    #[default]
370    S256,
371    /// Plain (not recommended, for legacy support only).
372    Plain,
373}
374
375impl std::fmt::Display for CodeChallengeMethod {
376    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
377        match self {
378            Self::S256 => write!(f, "S256"),
379            Self::Plain => write!(f, "plain"),
380        }
381    }
382}
383
384/// PKCE code verifier and challenge.
385#[derive(Debug, Clone)]
386pub struct PkceChallenge {
387    /// The code verifier (random string).
388    pub verifier: String,
389    /// The code challenge (derived from verifier).
390    pub challenge: String,
391    /// The challenge method used.
392    pub method: CodeChallengeMethod,
393}
394
395impl PkceChallenge {
396    /// Generate a new PKCE challenge using S256.
397    #[must_use]
398    pub fn new() -> Self {
399        Self::with_method(CodeChallengeMethod::S256)
400    }
401
402    /// Generate a PKCE challenge with the specified method.
403    #[must_use]
404    pub fn with_method(method: CodeChallengeMethod) -> Self {
405        use base64::Engine;
406        use rand::Rng;
407
408        // Generate a random 32-byte verifier
409        let mut rng = rand::thread_rng();
410        let verifier_bytes: [u8; 32] = rng.r#gen();
411        let verifier = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(verifier_bytes);
412
413        let challenge = match method {
414            CodeChallengeMethod::S256 => {
415                use sha2::{Digest, Sha256};
416                let hash = Sha256::digest(verifier.as_bytes());
417                base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hash)
418            }
419            CodeChallengeMethod::Plain => verifier.clone(),
420        };
421
422        Self {
423            verifier,
424            challenge,
425            method,
426        }
427    }
428
429    /// Verify a code verifier against a challenge.
430    #[must_use]
431    pub fn verify(verifier: &str, challenge: &str, method: CodeChallengeMethod) -> bool {
432        use base64::Engine;
433
434        let computed = match method {
435            CodeChallengeMethod::S256 => {
436                use sha2::{Digest, Sha256};
437                let hash = Sha256::digest(verifier.as_bytes());
438                base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hash)
439            }
440            CodeChallengeMethod::Plain => verifier.to_string(),
441        };
442
443        computed == challenge
444    }
445}
446
447impl Default for PkceChallenge {
448    fn default() -> Self {
449        Self::new()
450    }
451}
452
453/// Authorization request parameters.
454#[derive(Debug, Clone, Serialize, Deserialize)]
455pub struct AuthorizationRequest {
456    /// The response type (must be "code" for authorization code flow).
457    pub response_type: String,
458    /// The client identifier.
459    pub client_id: String,
460    /// The redirect URI.
461    #[serde(skip_serializing_if = "Option::is_none")]
462    pub redirect_uri: Option<String>,
463    /// The requested scope.
464    #[serde(skip_serializing_if = "Option::is_none")]
465    pub scope: Option<String>,
466    /// State parameter for CSRF protection.
467    #[serde(skip_serializing_if = "Option::is_none")]
468    pub state: Option<String>,
469    /// PKCE code challenge.
470    pub code_challenge: String,
471    /// PKCE code challenge method.
472    pub code_challenge_method: String,
473    /// Resource indicator (RFC 8707) - REQUIRED by MCP.
474    pub resource: String,
475}
476
477impl AuthorizationRequest {
478    /// Create a new authorization request with PKCE.
479    #[must_use]
480    pub fn new(
481        client_id: impl Into<String>,
482        pkce: &PkceChallenge,
483        resource: impl Into<String>,
484    ) -> Self {
485        Self {
486            response_type: "code".to_string(),
487            client_id: client_id.into(),
488            redirect_uri: None,
489            scope: None,
490            state: None,
491            code_challenge: pkce.challenge.clone(),
492            code_challenge_method: pkce.method.to_string(),
493            resource: resource.into(),
494        }
495    }
496
497    /// Set the redirect URI.
498    #[must_use]
499    pub fn with_redirect_uri(mut self, uri: impl Into<String>) -> Self {
500        self.redirect_uri = Some(uri.into());
501        self
502    }
503
504    /// Set the requested scope.
505    #[must_use]
506    pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
507        self.scope = Some(scope.into());
508        self
509    }
510
511    /// Set the state parameter.
512    #[must_use]
513    pub fn with_state(mut self, state: impl Into<String>) -> Self {
514        self.state = Some(state.into());
515        self
516    }
517
518    /// Build the authorization URL.
519    #[must_use]
520    pub fn build_url(&self, authorization_endpoint: &str) -> Option<String> {
521        let mut url = url::Url::parse(authorization_endpoint).ok()?;
522
523        {
524            let mut query = url.query_pairs_mut();
525            query.append_pair("response_type", &self.response_type);
526            query.append_pair("client_id", &self.client_id);
527            query.append_pair("code_challenge", &self.code_challenge);
528            query.append_pair("code_challenge_method", &self.code_challenge_method);
529            query.append_pair("resource", &self.resource);
530
531            if let Some(ref uri) = self.redirect_uri {
532                query.append_pair("redirect_uri", uri);
533            }
534            if let Some(ref scope) = self.scope {
535                query.append_pair("scope", scope);
536            }
537            if let Some(ref state) = self.state {
538                query.append_pair("state", state);
539            }
540        }
541
542        Some(url.to_string())
543    }
544}
545
546/// Token request for authorization code exchange.
547#[derive(Debug, Clone, Serialize, Deserialize)]
548pub struct TokenRequest {
549    /// The grant type.
550    pub grant_type: String,
551    /// The authorization code (for `authorization_code` grant).
552    #[serde(skip_serializing_if = "Option::is_none")]
553    pub code: Option<String>,
554    /// The redirect URI (must match the one in authorization request).
555    #[serde(skip_serializing_if = "Option::is_none")]
556    pub redirect_uri: Option<String>,
557    /// The client identifier.
558    pub client_id: String,
559    /// The client secret (for confidential clients).
560    #[serde(skip_serializing_if = "Option::is_none")]
561    pub client_secret: Option<String>,
562    /// PKCE code verifier.
563    #[serde(skip_serializing_if = "Option::is_none")]
564    pub code_verifier: Option<String>,
565    /// Resource indicator (RFC 8707).
566    #[serde(skip_serializing_if = "Option::is_none")]
567    pub resource: Option<String>,
568    /// Refresh token (for `refresh_token` grant).
569    #[serde(skip_serializing_if = "Option::is_none")]
570    pub refresh_token: Option<String>,
571    /// Requested scope.
572    #[serde(skip_serializing_if = "Option::is_none")]
573    pub scope: Option<String>,
574}
575
576impl TokenRequest {
577    /// Create a token request for authorization code exchange.
578    #[must_use]
579    pub fn authorization_code(
580        code: impl Into<String>,
581        client_id: impl Into<String>,
582        code_verifier: impl Into<String>,
583        resource: impl Into<String>,
584    ) -> Self {
585        Self {
586            grant_type: "authorization_code".to_string(),
587            code: Some(code.into()),
588            redirect_uri: None,
589            client_id: client_id.into(),
590            client_secret: None,
591            code_verifier: Some(code_verifier.into()),
592            resource: Some(resource.into()),
593            refresh_token: None,
594            scope: None,
595        }
596    }
597
598    /// Create a token request for client credentials grant.
599    #[must_use]
600    pub fn client_credentials(
601        client_id: impl Into<String>,
602        client_secret: impl Into<String>,
603        resource: impl Into<String>,
604    ) -> Self {
605        Self {
606            grant_type: "client_credentials".to_string(),
607            code: None,
608            redirect_uri: None,
609            client_id: client_id.into(),
610            client_secret: Some(client_secret.into()),
611            code_verifier: None,
612            resource: Some(resource.into()),
613            refresh_token: None,
614            scope: None,
615        }
616    }
617
618    /// Create a token request for refresh token grant.
619    #[must_use]
620    pub fn refresh(refresh_token: impl Into<String>, client_id: impl Into<String>) -> Self {
621        Self {
622            grant_type: "refresh_token".to_string(),
623            code: None,
624            redirect_uri: None,
625            client_id: client_id.into(),
626            client_secret: None,
627            code_verifier: None,
628            resource: None,
629            refresh_token: Some(refresh_token.into()),
630            scope: None,
631        }
632    }
633
634    /// Set the redirect URI.
635    #[must_use]
636    pub fn with_redirect_uri(mut self, uri: impl Into<String>) -> Self {
637        self.redirect_uri = Some(uri.into());
638        self
639    }
640
641    /// Set the requested scope.
642    #[must_use]
643    pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
644        self.scope = Some(scope.into());
645        self
646    }
647}
648
649/// Token response from the authorization server.
650#[derive(Debug, Clone, Serialize, Deserialize)]
651pub struct TokenResponse {
652    /// The access token.
653    pub access_token: String,
654    /// The token type (typically "Bearer").
655    pub token_type: String,
656    /// The token lifetime in seconds.
657    #[serde(skip_serializing_if = "Option::is_none")]
658    pub expires_in: Option<u64>,
659    /// The refresh token.
660    #[serde(skip_serializing_if = "Option::is_none")]
661    pub refresh_token: Option<String>,
662    /// The granted scope.
663    #[serde(skip_serializing_if = "Option::is_none")]
664    pub scope: Option<String>,
665}
666
667impl TokenResponse {
668    /// Check if the token is expired.
669    #[must_use]
670    pub fn is_expired(&self, issued_at: SystemTime) -> bool {
671        if let Some(expires_in) = self.expires_in {
672            let expiry = issued_at + Duration::from_secs(expires_in);
673            SystemTime::now() >= expiry
674        } else {
675            false // No expiration means never expires
676        }
677    }
678}
679
680/// Access token with metadata for client-side storage.
681#[derive(Debug, Clone)]
682pub struct StoredToken {
683    /// The token response.
684    pub token: TokenResponse,
685    /// When the token was issued.
686    pub issued_at: SystemTime,
687    /// The resource this token is bound to.
688    pub resource: String,
689}
690
691impl StoredToken {
692    /// Create a new stored token.
693    #[must_use]
694    pub fn new(token: TokenResponse, resource: impl Into<String>) -> Self {
695        Self {
696            token,
697            issued_at: SystemTime::now(),
698            resource: resource.into(),
699        }
700    }
701
702    /// Check if the token is expired.
703    #[must_use]
704    pub fn is_expired(&self) -> bool {
705        self.token.is_expired(self.issued_at)
706    }
707
708    /// Check if the token will expire within the given duration.
709    #[must_use]
710    pub fn expires_within(&self, duration: Duration) -> bool {
711        if let Some(expires_in) = self.token.expires_in {
712            let expiry = self.issued_at + Duration::from_secs(expires_in);
713            SystemTime::now() + duration >= expiry
714        } else {
715            false
716        }
717    }
718
719    /// Get the Authorization header value.
720    #[must_use]
721    pub fn authorization_header(&self) -> String {
722        format!("Bearer {}", self.token.access_token)
723    }
724}
725
726/// WWW-Authenticate header builder for 401 responses.
727#[derive(Debug, Clone)]
728pub struct WwwAuthenticate {
729    /// The authentication realm.
730    pub realm: Option<String>,
731    /// The resource metadata URL.
732    pub resource_metadata: String,
733    /// The error code.
734    pub error: Option<OAuthError>,
735    /// The error description.
736    pub error_description: Option<String>,
737}
738
739impl WwwAuthenticate {
740    /// Create a new WWW-Authenticate header.
741    #[must_use]
742    pub fn new(resource_metadata: impl Into<String>) -> Self {
743        Self {
744            realm: None,
745            resource_metadata: resource_metadata.into(),
746            error: None,
747            error_description: None,
748        }
749    }
750
751    /// Set the realm.
752    #[must_use]
753    pub fn with_realm(mut self, realm: impl Into<String>) -> Self {
754        self.realm = Some(realm.into());
755        self
756    }
757
758    /// Set the error.
759    #[must_use]
760    pub const fn with_error(mut self, error: OAuthError) -> Self {
761        self.error = Some(error);
762        self
763    }
764
765    /// Set the error description.
766    #[must_use]
767    pub fn with_error_description(mut self, description: impl Into<String>) -> Self {
768        self.error_description = Some(description.into());
769        self
770    }
771
772    /// Build the header value string per RFC 9728.
773    #[must_use]
774    pub fn to_header_value(&self) -> String {
775        let mut parts = vec![format!(
776            "Bearer resource_metadata=\"{}\"",
777            self.resource_metadata
778        )];
779
780        if let Some(ref realm) = self.realm {
781            parts.push(format!("realm=\"{realm}\""));
782        }
783        if let Some(ref error) = self.error {
784            parts.push(format!("error=\"{error}\""));
785        }
786        if let Some(ref desc) = self.error_description {
787            parts.push(format!("error_description=\"{desc}\""));
788        }
789
790        parts.join(", ")
791    }
792
793    /// Parse a WWW-Authenticate header value.
794    #[must_use]
795    pub fn parse(header_value: &str) -> Option<Self> {
796        if !header_value.starts_with("Bearer ") {
797            return None;
798        }
799
800        let params = &header_value[7..]; // Skip "Bearer "
801        let mut resource_metadata = None;
802        let mut realm = None;
803        let mut error = None;
804        let mut error_description = None;
805
806        for part in params.split(", ") {
807            if let Some((key, value)) = part.split_once('=') {
808                let value = value.trim_matches('"');
809                match key.trim() {
810                    "resource_metadata" => resource_metadata = Some(value.to_string()),
811                    "realm" => realm = Some(value.to_string()),
812                    "error" => {
813                        error = match value {
814                            "invalid_request" => Some(OAuthError::InvalidRequest),
815                            "invalid_token" => Some(OAuthError::InvalidToken),
816                            "insufficient_scope" => Some(OAuthError::InsufficientScope),
817                            _ => None,
818                        };
819                    }
820                    "error_description" => error_description = Some(value.to_string()),
821                    _ => {}
822                }
823            }
824        }
825
826        resource_metadata.map(|rm| Self {
827            realm,
828            resource_metadata: rm,
829            error,
830            error_description,
831        })
832    }
833}
834
835/// Client authorization configuration.
836#[derive(Debug, Clone)]
837pub struct AuthorizationConfig {
838    /// The authorization server URL.
839    pub authorization_server: String,
840    /// The client identifier.
841    pub client_id: String,
842    /// The client secret (for confidential clients).
843    pub client_secret: Option<String>,
844    /// The redirect URI for authorization code flow.
845    pub redirect_uri: Option<String>,
846    /// The target resource (MCP server URL).
847    pub resource: Option<String>,
848    /// The requested scopes.
849    pub scopes: Vec<String>,
850}
851
852impl AuthorizationConfig {
853    /// Create a new authorization configuration.
854    #[must_use]
855    pub fn new(authorization_server: impl Into<String>) -> Self {
856        Self {
857            authorization_server: authorization_server.into(),
858            client_id: String::new(),
859            client_secret: None,
860            redirect_uri: None,
861            resource: None,
862            scopes: Vec::new(),
863        }
864    }
865
866    /// Set the client ID.
867    #[must_use]
868    pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
869        self.client_id = client_id.into();
870        self
871    }
872
873    /// Set the client secret.
874    #[must_use]
875    pub fn with_client_secret(mut self, secret: impl Into<String>) -> Self {
876        self.client_secret = Some(secret.into());
877        self
878    }
879
880    /// Set the redirect URI.
881    #[must_use]
882    pub fn with_redirect_uri(mut self, uri: impl Into<String>) -> Self {
883        self.redirect_uri = Some(uri.into());
884        self
885    }
886
887    /// Set the target resource (MCP server URL).
888    #[must_use]
889    pub fn with_resource(mut self, resource: impl Into<String>) -> Self {
890        self.resource = Some(resource.into());
891        self
892    }
893
894    /// Add a scope.
895    #[must_use]
896    pub fn with_scope(mut self, scope: impl Into<String>) -> Self {
897        self.scopes.push(scope.into());
898        self
899    }
900
901    /// Check if this is a public client (no client secret).
902    #[must_use]
903    pub const fn is_public_client(&self) -> bool {
904        self.client_secret.is_none()
905    }
906}
907
908/// Dynamic Client Registration request per RFC 7591.
909#[derive(Debug, Clone, Serialize, Deserialize)]
910pub struct ClientRegistrationRequest {
911    /// Requested redirect URIs.
912    #[serde(skip_serializing_if = "Option::is_none")]
913    pub redirect_uris: Option<Vec<String>>,
914    /// Token endpoint authentication method.
915    #[serde(skip_serializing_if = "Option::is_none")]
916    pub token_endpoint_auth_method: Option<String>,
917    /// Requested grant types.
918    #[serde(skip_serializing_if = "Option::is_none")]
919    pub grant_types: Option<Vec<String>>,
920    /// Requested response types.
921    #[serde(skip_serializing_if = "Option::is_none")]
922    pub response_types: Option<Vec<String>>,
923    /// Client name.
924    #[serde(skip_serializing_if = "Option::is_none")]
925    pub client_name: Option<String>,
926    /// Client URI.
927    #[serde(skip_serializing_if = "Option::is_none")]
928    pub client_uri: Option<String>,
929    /// Software identifier.
930    #[serde(skip_serializing_if = "Option::is_none")]
931    pub software_id: Option<String>,
932    /// Software version.
933    #[serde(skip_serializing_if = "Option::is_none")]
934    pub software_version: Option<String>,
935}
936
937impl ClientRegistrationRequest {
938    /// Create a new client registration request.
939    #[must_use]
940    pub fn new() -> Self {
941        Self {
942            redirect_uris: None,
943            token_endpoint_auth_method: Some("none".to_string()), // Public client
944            grant_types: Some(vec!["authorization_code".to_string()]),
945            response_types: Some(vec!["code".to_string()]),
946            client_name: None,
947            client_uri: None,
948            software_id: None,
949            software_version: None,
950        }
951    }
952
953    /// Set redirect URIs.
954    #[must_use]
955    pub fn with_redirect_uris(mut self, uris: impl IntoIterator<Item = impl Into<String>>) -> Self {
956        self.redirect_uris = Some(uris.into_iter().map(Into::into).collect());
957        self
958    }
959
960    /// Set the client name.
961    #[must_use]
962    pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
963        self.client_name = Some(name.into());
964        self
965    }
966
967    /// Set the software ID.
968    #[must_use]
969    pub fn with_software_id(mut self, id: impl Into<String>) -> Self {
970        self.software_id = Some(id.into());
971        self
972    }
973}
974
975impl Default for ClientRegistrationRequest {
976    fn default() -> Self {
977        Self::new()
978    }
979}
980
981/// Dynamic Client Registration response per RFC 7591.
982#[derive(Debug, Clone, Serialize, Deserialize)]
983pub struct ClientRegistrationResponse {
984    /// The assigned client identifier.
985    pub client_id: String,
986    /// The client secret (if applicable).
987    #[serde(skip_serializing_if = "Option::is_none")]
988    pub client_secret: Option<String>,
989    /// Client secret expiration time.
990    #[serde(skip_serializing_if = "Option::is_none")]
991    pub client_secret_expires_at: Option<u64>,
992    /// Client ID issued at time.
993    #[serde(skip_serializing_if = "Option::is_none")]
994    pub client_id_issued_at: Option<u64>,
995    /// All other registered metadata echoed back.
996    #[serde(flatten)]
997    pub metadata: HashMap<String, serde_json::Value>,
998}
999
1000#[cfg(test)]
1001mod tests {
1002    use super::*;
1003
1004    #[test]
1005    fn test_protected_resource_metadata() {
1006        let metadata = ProtectedResourceMetadata::new("https://mcp.example.com")
1007            .with_authorization_server("https://auth.example.com")
1008            .with_scopes(["mcp:read", "mcp:write"]);
1009
1010        assert_eq!(metadata.resource, "https://mcp.example.com");
1011        assert_eq!(metadata.authorization_servers.len(), 1);
1012        assert!(metadata.validate().is_ok());
1013    }
1014
1015    #[test]
1016    fn test_protected_resource_metadata_validation() {
1017        let metadata = ProtectedResourceMetadata::new("https://mcp.example.com");
1018        assert!(metadata.validate().is_err()); // No auth servers
1019
1020        let metadata = metadata.with_authorization_server("https://auth.example.com");
1021        assert!(metadata.validate().is_ok());
1022    }
1023
1024    #[test]
1025    fn test_authorization_server_metadata() {
1026        let metadata = AuthorizationServerMetadata::from_issuer("https://auth.example.com");
1027
1028        assert_eq!(metadata.issuer, "https://auth.example.com");
1029        assert_eq!(
1030            metadata.authorization_endpoint,
1031            "https://auth.example.com/authorize"
1032        );
1033        assert_eq!(metadata.token_endpoint, "https://auth.example.com/token");
1034    }
1035
1036    #[test]
1037    fn test_pkce_challenge() {
1038        let pkce = PkceChallenge::new();
1039
1040        // Verifier should be different from challenge for S256
1041        assert_ne!(pkce.verifier, pkce.challenge);
1042
1043        // Verification should work
1044        assert!(PkceChallenge::verify(
1045            &pkce.verifier,
1046            &pkce.challenge,
1047            CodeChallengeMethod::S256
1048        ));
1049
1050        // Wrong verifier should fail
1051        assert!(!PkceChallenge::verify(
1052            "wrong",
1053            &pkce.challenge,
1054            CodeChallengeMethod::S256
1055        ));
1056    }
1057
1058    #[test]
1059    fn test_authorization_request() -> Result<(), Box<dyn std::error::Error>> {
1060        let pkce = PkceChallenge::new();
1061        let request = AuthorizationRequest::new("client123", &pkce, "https://mcp.example.com")
1062            .with_redirect_uri("http://localhost:8080/callback")
1063            .with_scope("mcp:read")
1064            .with_state("random_state");
1065
1066        assert_eq!(request.client_id, "client123");
1067        assert_eq!(request.resource, "https://mcp.example.com");
1068
1069        let url = request.build_url("https://auth.example.com/authorize");
1070        assert!(url.is_some());
1071        let url = url.ok_or("Expected URL")?;
1072        assert!(url.contains("response_type=code"));
1073        assert!(url.contains("client_id=client123"));
1074        assert!(url.contains("resource="));
1075        Ok(())
1076    }
1077
1078    #[test]
1079    fn test_token_request_authorization_code() {
1080        let request = TokenRequest::authorization_code(
1081            "auth_code_123",
1082            "client123",
1083            "verifier123",
1084            "https://mcp.example.com",
1085        );
1086
1087        assert_eq!(request.grant_type, "authorization_code");
1088        assert_eq!(request.code, Some("auth_code_123".to_string()));
1089        assert_eq!(request.code_verifier, Some("verifier123".to_string()));
1090    }
1091
1092    #[test]
1093    fn test_token_request_client_credentials() {
1094        let request =
1095            TokenRequest::client_credentials("client123", "secret456", "https://mcp.example.com");
1096
1097        assert_eq!(request.grant_type, "client_credentials");
1098        assert_eq!(request.client_secret, Some("secret456".to_string()));
1099    }
1100
1101    #[test]
1102    fn test_www_authenticate_header() {
1103        let header =
1104            WwwAuthenticate::new("https://mcp.example.com/.well-known/oauth-protected-resource")
1105                .with_realm("mcp")
1106                .with_error(OAuthError::InvalidToken)
1107                .with_error_description("Token expired");
1108
1109        let value = header.to_header_value();
1110        assert!(value.starts_with("Bearer resource_metadata="));
1111        assert!(value.contains("realm=\"mcp\""));
1112        assert!(value.contains("error=\"invalid_token\""));
1113    }
1114
1115    #[test]
1116    fn test_www_authenticate_parse() -> Result<(), Box<dyn std::error::Error>> {
1117        let header_value = "Bearer resource_metadata=\"https://example.com/.well-known/oauth-protected-resource\", realm=\"mcp\"";
1118        let parsed = WwwAuthenticate::parse(header_value);
1119
1120        assert!(parsed.is_some());
1121        let parsed = parsed.ok_or("Expected parsed header")?;
1122        assert_eq!(
1123            parsed.resource_metadata,
1124            "https://example.com/.well-known/oauth-protected-resource"
1125        );
1126        assert_eq!(parsed.realm, Some("mcp".to_string()));
1127        Ok(())
1128    }
1129
1130    #[test]
1131    fn test_authorization_config() {
1132        let config = AuthorizationConfig::new("https://auth.example.com")
1133            .with_client_id("my-client")
1134            .with_resource("https://mcp.example.com")
1135            .with_scope("mcp:read");
1136
1137        assert!(config.is_public_client());
1138        assert_eq!(config.client_id, "my-client");
1139
1140        let config = config.with_client_secret("secret");
1141        assert!(!config.is_public_client());
1142    }
1143
1144    #[test]
1145    fn test_stored_token() {
1146        let token = TokenResponse {
1147            access_token: "access123".to_string(),
1148            token_type: "Bearer".to_string(),
1149            expires_in: Some(3600),
1150            refresh_token: Some("refresh456".to_string()),
1151            scope: Some("mcp:read".to_string()),
1152        };
1153
1154        let stored = StoredToken::new(token, "https://mcp.example.com");
1155        assert!(!stored.is_expired());
1156        assert_eq!(stored.authorization_header(), "Bearer access123");
1157    }
1158
1159    #[test]
1160    fn test_client_registration_request() {
1161        let request = ClientRegistrationRequest::new()
1162            .with_client_name("My MCP Client")
1163            .with_redirect_uris(["http://localhost:8080/callback"]);
1164
1165        assert_eq!(request.client_name, Some("My MCP Client".to_string()));
1166        assert!(request.redirect_uris.is_some());
1167    }
1168
1169    #[test]
1170    fn test_oauth_error_display() {
1171        assert_eq!(OAuthError::InvalidRequest.to_string(), "invalid_request");
1172        assert_eq!(OAuthError::InvalidToken.to_string(), "invalid_token");
1173        assert_eq!(
1174            OAuthError::InsufficientScope.to_string(),
1175            "insufficient_scope"
1176        );
1177    }
1178}