Skip to main content

mcpkit_core/auth/
jwt.rs

1//! JWT validation helpers for MCP authorization.
2//!
3//! This module provides JWT (JSON Web Token) validation functionality for MCP
4//! servers, including JWKS (JSON Web Key Set) fetching from authorization servers.
5//!
6//! # Features
7//!
8//! - JWT signature verification using RS256 and ES256 algorithms
9//! - JWKS fetching from authorization server endpoints
10//! - Standard claims validation (exp, iat, aud, iss)
11//! - Custom scope validation for MCP
12//!
13//! # Example
14//!
15//! ```rust,ignore
16//! use mcpkit_core::auth::jwt::{validate_token_with_fetch, TokenValidation};
17//!
18//! // Validate a token by fetching JWKS from the authorization server
19//! let validation = TokenValidation::new()
20//!     .with_issuer("https://auth.example.com")
21//!     .with_audience("https://mcp.example.com");
22//!
23//! let claims = validate_token_with_fetch(
24//!     "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
25//!     "https://auth.example.com/.well-known/jwks.json",
26//!     &validation,
27//! ).await?;
28//!
29//! println!("Token subject: {:?}", claims.sub);
30//! ```
31//!
32//! # Security Considerations
33//!
34//! - Always validate tokens before granting access to protected resources
35//! - Use HTTPS for JWKS endpoints to prevent MITM attacks
36//! - Consider implementing JWKS caching in production (not included here)
37//! - Validate issuer and audience claims to prevent token confusion attacks
38
39use serde::{Deserialize, Serialize};
40use std::collections::HashMap;
41
42/// Errors that can occur during JWT validation.
43#[derive(Debug, Clone, thiserror::Error)]
44pub enum JwtError {
45    /// The token format is invalid.
46    #[error("invalid token format: {message}")]
47    InvalidFormat {
48        /// Error message.
49        message: String,
50    },
51
52    /// The token signature is invalid.
53    #[error("invalid signature: {message}")]
54    InvalidSignature {
55        /// Error message.
56        message: String,
57    },
58
59    /// The token has expired.
60    #[error("token expired")]
61    Expired,
62
63    /// The token is not yet valid (nbf claim).
64    #[error("token not yet valid")]
65    NotYetValid,
66
67    /// The issuer claim is invalid.
68    #[error("invalid issuer: expected {expected}, got {actual}")]
69    InvalidIssuer {
70        /// Expected issuer.
71        expected: String,
72        /// Actual issuer.
73        actual: String,
74    },
75
76    /// The audience claim is invalid.
77    #[error("invalid audience: expected {expected}")]
78    InvalidAudience {
79        /// Expected audience.
80        expected: String,
81    },
82
83    /// A required claim is missing.
84    #[error("missing required claim: {claim}")]
85    MissingClaim {
86        /// The missing claim name.
87        claim: String,
88    },
89
90    /// The required scope is not present.
91    #[error("insufficient scope: required {required}")]
92    InsufficientScope {
93        /// The required scope.
94        required: String,
95    },
96
97    /// Failed to fetch JWKS.
98    #[error("JWKS fetch failed: {message}")]
99    JwksFetchError {
100        /// Error message.
101        message: String,
102    },
103
104    /// No matching key found in JWKS.
105    #[error("no matching key found for kid: {kid}")]
106    NoMatchingKey {
107        /// The key ID.
108        kid: String,
109    },
110
111    /// The algorithm is not supported.
112    #[error("unsupported algorithm: {algorithm}")]
113    UnsupportedAlgorithm {
114        /// The algorithm.
115        algorithm: String,
116    },
117
118    /// The token's algorithm is not in the relying party's allowlist.
119    #[error("algorithm not allowed: {algorithm}")]
120    AlgorithmNotAllowed {
121        /// The rejected algorithm.
122        algorithm: String,
123    },
124}
125
126/// JWT validation configuration.
127#[derive(Debug, Clone, Default)]
128pub struct TokenValidation {
129    /// Expected issuer (iss claim).
130    pub issuer: Option<String>,
131    /// Expected audience (aud claim).
132    pub audience: Option<String>,
133    /// Required scopes (space-separated in scope claim).
134    pub required_scopes: Vec<String>,
135    /// Whether to validate expiration.
136    pub validate_exp: bool,
137    /// Clock skew tolerance in seconds for time validation.
138    pub leeway_seconds: u64,
139    /// Custom claims that must be present.
140    pub required_claims: Vec<String>,
141    /// Accepted signing algorithms by name (e.g. `"RS256"`).
142    ///
143    /// When non-empty, the token header's `alg` must be one of these, letting
144    /// the relying party pin acceptable algorithms rather than trusting the
145    /// value in the token header (RFC 8725 §3.1). Empty accepts any supported
146    /// algorithm.
147    pub allowed_algorithms: Vec<String>,
148}
149
150impl TokenValidation {
151    /// Create a new validation configuration with defaults.
152    #[must_use]
153    pub fn new() -> Self {
154        Self {
155            issuer: None,
156            audience: None,
157            required_scopes: Vec::new(),
158            validate_exp: true,
159            leeway_seconds: 60, // 1 minute default leeway
160            required_claims: Vec::new(),
161            allowed_algorithms: Vec::new(),
162        }
163    }
164
165    /// Set the expected issuer.
166    #[must_use]
167    pub fn with_issuer(mut self, issuer: impl Into<String>) -> Self {
168        self.issuer = Some(issuer.into());
169        self
170    }
171
172    /// Set the expected audience.
173    #[must_use]
174    pub fn with_audience(mut self, audience: impl Into<String>) -> Self {
175        self.audience = Some(audience.into());
176        self
177    }
178
179    /// Add a required scope.
180    #[must_use]
181    pub fn with_required_scope(mut self, scope: impl Into<String>) -> Self {
182        self.required_scopes.push(scope.into());
183        self
184    }
185
186    /// Set multiple required scopes.
187    #[must_use]
188    pub fn with_required_scopes(
189        mut self,
190        scopes: impl IntoIterator<Item = impl Into<String>>,
191    ) -> Self {
192        self.required_scopes = scopes.into_iter().map(Into::into).collect();
193        self
194    }
195
196    /// Set the clock skew leeway.
197    #[must_use]
198    pub const fn with_leeway(mut self, seconds: u64) -> Self {
199        self.leeway_seconds = seconds;
200        self
201    }
202
203    /// Disable expiration validation (not recommended).
204    #[must_use]
205    pub const fn without_exp_validation(mut self) -> Self {
206        self.validate_exp = false;
207        self
208    }
209
210    /// Add a required custom claim.
211    #[must_use]
212    pub fn with_required_claim(mut self, claim: impl Into<String>) -> Self {
213        self.required_claims.push(claim.into());
214        self
215    }
216
217    /// Restrict accepted signing algorithms to the given names (e.g. `"RS256"`).
218    ///
219    /// Pins the algorithms the relying party will accept so a token cannot
220    /// dictate its own verification algorithm (RFC 8725 §3.1).
221    #[must_use]
222    pub fn with_allowed_algorithms(
223        mut self,
224        algorithms: impl IntoIterator<Item = impl Into<String>>,
225    ) -> Self {
226        self.allowed_algorithms = algorithms.into_iter().map(Into::into).collect();
227        self
228    }
229}
230
231/// Standard JWT claims.
232#[derive(Debug, Clone, Serialize, Deserialize)]
233pub struct TokenClaims {
234    /// Issuer (iss).
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub iss: Option<String>,
237
238    /// Subject (sub).
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub sub: Option<String>,
241
242    /// Audience (aud) - can be string or array.
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub aud: Option<Audience>,
245
246    /// Expiration time (exp) - Unix timestamp.
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub exp: Option<u64>,
249
250    /// Issued at (iat) - Unix timestamp.
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub iat: Option<u64>,
253
254    /// Not before (nbf) - Unix timestamp.
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub nbf: Option<u64>,
257
258    /// JWT ID (jti).
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub jti: Option<String>,
261
262    /// OAuth scope claim.
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub scope: Option<String>,
265
266    /// Additional custom claims.
267    #[serde(flatten)]
268    pub extra: HashMap<String, serde_json::Value>,
269}
270
271impl TokenClaims {
272    /// Check if the token has a specific scope.
273    #[must_use]
274    pub fn has_scope(&self, scope: &str) -> bool {
275        self.scope
276            .as_ref()
277            .is_some_and(|s| s.split_whitespace().any(|part| part == scope))
278    }
279
280    /// Get all scopes as a vector.
281    #[must_use]
282    pub fn scopes(&self) -> Vec<&str> {
283        self.scope
284            .as_ref()
285            .map(|s| s.split_whitespace().collect())
286            .unwrap_or_default()
287    }
288}
289
290/// Audience claim which can be a single string or array.
291#[derive(Debug, Clone, Serialize, Deserialize)]
292#[serde(untagged)]
293pub enum Audience {
294    /// Single audience.
295    Single(String),
296    /// Multiple audiences.
297    Multiple(Vec<String>),
298}
299
300impl Audience {
301    /// Check if the audience contains a specific value.
302    #[must_use]
303    pub fn contains(&self, aud: &str) -> bool {
304        match self {
305            Self::Single(s) => s == aud,
306            Self::Multiple(v) => v.iter().any(|a| a == aud),
307        }
308    }
309}
310
311/// JSON Web Key Set (JWKS) response.
312#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct JwksSet {
314    /// The keys in the set.
315    pub keys: Vec<Jwk>,
316}
317
318impl JwksSet {
319    /// Find a key by its ID.
320    #[must_use]
321    pub fn find_key(&self, kid: &str) -> Option<&Jwk> {
322        self.keys.iter().find(|k| k.kid.as_deref() == Some(kid))
323    }
324
325    /// Get the first key (useful when there's only one key).
326    #[must_use]
327    pub fn first_key(&self) -> Option<&Jwk> {
328        self.keys.first()
329    }
330}
331
332/// JSON Web Key (JWK).
333#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct Jwk {
335    /// Key type (kty) - "RSA" or "EC".
336    pub kty: String,
337
338    /// Key ID (kid).
339    #[serde(skip_serializing_if = "Option::is_none")]
340    pub kid: Option<String>,
341
342    /// Key use (use) - "sig" for signing.
343    #[serde(rename = "use", skip_serializing_if = "Option::is_none")]
344    pub key_use: Option<String>,
345
346    /// Algorithm (alg).
347    #[serde(skip_serializing_if = "Option::is_none")]
348    pub alg: Option<String>,
349
350    // RSA parameters
351    /// RSA modulus (n).
352    #[serde(skip_serializing_if = "Option::is_none")]
353    pub n: Option<String>,
354
355    /// RSA exponent (e).
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub e: Option<String>,
358
359    // EC parameters
360    /// EC curve (crv).
361    #[serde(skip_serializing_if = "Option::is_none")]
362    pub crv: Option<String>,
363
364    /// EC x coordinate.
365    #[serde(skip_serializing_if = "Option::is_none")]
366    pub x: Option<String>,
367
368    /// EC y coordinate.
369    #[serde(skip_serializing_if = "Option::is_none")]
370    pub y: Option<String>,
371}
372
373impl Jwk {
374    /// Check if this is an RSA key.
375    #[must_use]
376    pub fn is_rsa(&self) -> bool {
377        self.kty == "RSA"
378    }
379
380    /// Check if this is an EC key.
381    #[must_use]
382    pub fn is_ec(&self) -> bool {
383        self.kty == "EC"
384    }
385
386    /// Check if this key is for signing.
387    #[must_use]
388    pub fn is_signing_key(&self) -> bool {
389        self.key_use.as_deref() == Some("sig") || self.key_use.is_none()
390    }
391}
392
393/// JWT header.
394#[derive(Debug, Clone, Serialize, Deserialize)]
395pub struct JwtHeader {
396    /// Algorithm (alg).
397    pub alg: String,
398
399    /// Type (typ).
400    #[serde(skip_serializing_if = "Option::is_none")]
401    pub typ: Option<String>,
402
403    /// Key ID (kid).
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub kid: Option<String>,
406}
407
408/// Decode JWT header without verification.
409///
410/// This is useful for extracting the key ID before fetching JWKS.
411///
412/// # Errors
413///
414/// Returns `JwtError::InvalidFormat` if the token format is invalid.
415pub fn decode_header(token: &str) -> Result<JwtHeader, JwtError> {
416    use base64::Engine;
417
418    let parts: Vec<&str> = token.split('.').collect();
419    if parts.len() != 3 {
420        return Err(JwtError::InvalidFormat {
421            message: "token must have 3 parts".to_string(),
422        });
423    }
424
425    let header_json = base64::engine::general_purpose::URL_SAFE_NO_PAD
426        .decode(parts[0])
427        .map_err(|e| JwtError::InvalidFormat {
428            message: format!("invalid header encoding: {e}"),
429        })?;
430
431    serde_json::from_slice(&header_json).map_err(|e| JwtError::InvalidFormat {
432        message: format!("invalid header JSON: {e}"),
433    })
434}
435
436/// Decode JWT claims without verification.
437///
438/// **WARNING**: This does not verify the signature! Only use for debugging or
439/// when you've already validated the token through other means.
440///
441/// # Errors
442///
443/// Returns `JwtError::InvalidFormat` if the token format is invalid.
444pub fn decode_claims_unverified(token: &str) -> Result<TokenClaims, JwtError> {
445    use base64::Engine;
446
447    let parts: Vec<&str> = token.split('.').collect();
448    if parts.len() != 3 {
449        return Err(JwtError::InvalidFormat {
450            message: "token must have 3 parts".to_string(),
451        });
452    }
453
454    let payload_json = base64::engine::general_purpose::URL_SAFE_NO_PAD
455        .decode(parts[1])
456        .map_err(|e| JwtError::InvalidFormat {
457            message: format!("invalid payload encoding: {e}"),
458        })?;
459
460    serde_json::from_slice(&payload_json).map_err(|e| JwtError::InvalidFormat {
461        message: format!("invalid payload JSON: {e}"),
462    })
463}
464
465/// Validate claims against the validation configuration.
466///
467/// This does not verify the signature - use `validate_token` for full validation.
468///
469/// # Errors
470///
471/// Returns an error if any validation check fails.
472pub fn validate_claims(claims: &TokenClaims, validation: &TokenValidation) -> Result<(), JwtError> {
473    let now = std::time::SystemTime::now()
474        .duration_since(std::time::UNIX_EPOCH)
475        .map_or(0, |d| d.as_secs());
476
477    // Validate expiration (use saturating arithmetic to prevent overflow)
478    if validation.validate_exp {
479        if let Some(exp) = claims.exp {
480            if now > exp.saturating_add(validation.leeway_seconds) {
481                return Err(JwtError::Expired);
482            }
483        }
484    }
485
486    // Validate not before (use saturating arithmetic to prevent overflow)
487    if let Some(nbf) = claims.nbf {
488        if now.saturating_add(validation.leeway_seconds) < nbf {
489            return Err(JwtError::NotYetValid);
490        }
491    }
492
493    // Validate issuer
494    if let Some(ref expected_iss) = validation.issuer {
495        match &claims.iss {
496            Some(actual_iss) if actual_iss != expected_iss => {
497                return Err(JwtError::InvalidIssuer {
498                    expected: expected_iss.clone(),
499                    actual: actual_iss.clone(),
500                });
501            }
502            None => {
503                return Err(JwtError::MissingClaim {
504                    claim: "iss".to_string(),
505                });
506            }
507            _ => {}
508        }
509    }
510
511    // Validate audience
512    if let Some(ref expected_aud) = validation.audience {
513        match &claims.aud {
514            Some(aud) if aud.contains(expected_aud) => {}
515            Some(_) => {
516                return Err(JwtError::InvalidAudience {
517                    expected: expected_aud.clone(),
518                });
519            }
520            None => {
521                return Err(JwtError::MissingClaim {
522                    claim: "aud".to_string(),
523                });
524            }
525        }
526    }
527
528    // Validate required scopes
529    for required_scope in &validation.required_scopes {
530        if !claims.has_scope(required_scope) {
531            return Err(JwtError::InsufficientScope {
532                required: required_scope.clone(),
533            });
534        }
535    }
536
537    // Validate required custom claims
538    check_required_claims(claims, &validation.required_claims)?;
539
540    Ok(())
541}
542
543/// Ensure every configured required claim is present on the token.
544///
545/// Checks custom claims (in `extra`) as well as the registered claims, so
546/// `required_claims` works for arbitrary names. We enforce this ourselves
547/// rather than delegating to `jsonwebtoken::Validation::set_required_spec_claims`,
548/// which only understands the five registered claims and *replaces* (rather
549/// than extends) the required set — silently ignoring custom claims and
550/// dropping the default `exp`-presence requirement.
551fn check_required_claims(claims: &TokenClaims, required: &[String]) -> Result<(), JwtError> {
552    for claim_name in required {
553        let present = claims.extra.contains_key(claim_name)
554            || match claim_name.as_str() {
555                "iss" => claims.iss.is_some(),
556                "sub" => claims.sub.is_some(),
557                "aud" => claims.aud.is_some(),
558                "exp" => claims.exp.is_some(),
559                "iat" => claims.iat.is_some(),
560                "nbf" => claims.nbf.is_some(),
561                "jti" => claims.jti.is_some(),
562                "scope" => claims.scope.is_some(),
563                _ => false,
564            };
565        if !present {
566            return Err(JwtError::MissingClaim {
567                claim: claim_name.clone(),
568            });
569        }
570    }
571    Ok(())
572}
573
574/// Supported JWT signing algorithms.
575#[cfg(feature = "jwt")]
576#[derive(Debug, Clone, Copy, PartialEq, Eq)]
577pub enum JwtAlgorithm {
578    /// RSA with SHA-256 (most common for OAuth 2.1).
579    RS256,
580    /// RSA with SHA-384.
581    RS384,
582    /// RSA with SHA-512.
583    RS512,
584    /// ECDSA with P-256 curve and SHA-256.
585    ES256,
586    /// ECDSA with P-384 curve and SHA-384.
587    ES384,
588}
589
590#[cfg(feature = "jwt")]
591impl JwtAlgorithm {
592    /// Parse algorithm from JWT header "alg" value.
593    #[must_use]
594    pub fn parse(alg: &str) -> Option<Self> {
595        match alg {
596            "RS256" => Some(Self::RS256),
597            "RS384" => Some(Self::RS384),
598            "RS512" => Some(Self::RS512),
599            "ES256" => Some(Self::ES256),
600            "ES384" => Some(Self::ES384),
601            _ => None,
602        }
603    }
604
605    /// Convert to jsonwebtoken Algorithm.
606    fn to_jsonwebtoken_algorithm(self) -> jsonwebtoken::Algorithm {
607        match self {
608            Self::RS256 => jsonwebtoken::Algorithm::RS256,
609            Self::RS384 => jsonwebtoken::Algorithm::RS384,
610            Self::RS512 => jsonwebtoken::Algorithm::RS512,
611            Self::ES256 => jsonwebtoken::Algorithm::ES256,
612            Self::ES384 => jsonwebtoken::Algorithm::ES384,
613        }
614    }
615}
616
617#[cfg(feature = "jwt")]
618impl std::fmt::Display for JwtAlgorithm {
619    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
620        match self {
621            Self::RS256 => write!(f, "RS256"),
622            Self::RS384 => write!(f, "RS384"),
623            Self::RS512 => write!(f, "RS512"),
624            Self::ES256 => write!(f, "ES256"),
625            Self::ES384 => write!(f, "ES384"),
626        }
627    }
628}
629
630/// Create a jsonwebtoken DecodingKey from our Jwk type.
631#[cfg(feature = "jwt")]
632fn create_decoding_key(
633    jwk: &Jwk,
634    alg: JwtAlgorithm,
635) -> Result<jsonwebtoken::DecodingKey, JwtError> {
636    match alg {
637        JwtAlgorithm::RS256 | JwtAlgorithm::RS384 | JwtAlgorithm::RS512 => {
638            let n = jwk.n.as_ref().ok_or_else(|| JwtError::InvalidFormat {
639                message: "RSA key missing 'n' component".to_string(),
640            })?;
641            let e = jwk.e.as_ref().ok_or_else(|| JwtError::InvalidFormat {
642                message: "RSA key missing 'e' component".to_string(),
643            })?;
644            jsonwebtoken::DecodingKey::from_rsa_components(n, e).map_err(|e| {
645                JwtError::InvalidFormat {
646                    message: format!("invalid RSA key: {e}"),
647                }
648            })
649        }
650        JwtAlgorithm::ES256 | JwtAlgorithm::ES384 => {
651            let x = jwk.x.as_ref().ok_or_else(|| JwtError::InvalidFormat {
652                message: "EC key missing 'x' component".to_string(),
653            })?;
654            let y = jwk.y.as_ref().ok_or_else(|| JwtError::InvalidFormat {
655                message: "EC key missing 'y' component".to_string(),
656            })?;
657            jsonwebtoken::DecodingKey::from_ec_components(x, y).map_err(|e| {
658                JwtError::InvalidFormat {
659                    message: format!("invalid EC key: {e}"),
660                }
661            })
662        }
663    }
664}
665
666/// Build jsonwebtoken Validation from our TokenValidation.
667#[cfg(feature = "jwt")]
668fn build_jwt_validation(
669    validation: &TokenValidation,
670    alg: JwtAlgorithm,
671) -> jsonwebtoken::Validation {
672    let mut jwt_validation = jsonwebtoken::Validation::new(alg.to_jsonwebtoken_algorithm());
673
674    // Set leeway for time-based claims
675    jwt_validation.leeway = validation.leeway_seconds;
676
677    // Set issuer validation
678    if let Some(ref iss) = validation.issuer {
679        jwt_validation.set_issuer(&[iss]);
680    }
681
682    // Set audience validation
683    if let Some(ref aud) = validation.audience {
684        jwt_validation.set_audience(&[aud]);
685    }
686
687    // Set expiration validation
688    jwt_validation.validate_exp = validation.validate_exp;
689
690    // Required custom claims are enforced after decoding (see
691    // `check_required_claims` in `validate_token`). We deliberately do not call
692    // `set_required_spec_claims` here: it only handles registered claims and
693    // replaces the required set, which would silently drop the default
694    // `exp`-presence requirement.
695
696    jwt_validation
697}
698
699/// Validate a JWT access token with full signature verification.
700///
701/// This function verifies the token's cryptographic signature using the provided
702/// JWKS and validates the claims against the validation configuration.
703///
704/// # Arguments
705///
706/// * `token` - The JWT access token to validate
707/// * `jwks` - The JSON Web Key Set containing public keys
708/// * `validation` - The validation configuration
709///
710/// # Algorithm Selection
711///
712/// The function automatically selects the correct key from the JWKS based on:
713/// 1. The `kid` (key ID) in the JWT header, if present
714/// 2. The `alg` (algorithm) in the JWT header
715///
716/// # Supported Algorithms
717///
718/// - **RS256, RS384, RS512**: RSA with SHA-256/384/512
719/// - **ES256, ES384**: ECDSA with P-256/P-384 curves
720///
721/// # Example
722///
723/// ```rust,ignore
724/// use mcpkit_core::auth::jwt::{validate_token, TokenValidation, JwksSet};
725///
726/// let jwks: JwksSet = fetch_jwks_from_somewhere();
727/// let validation = TokenValidation::new()
728///     .with_issuer("https://auth.example.com")
729///     .with_audience("https://mcp.example.com");
730///
731/// let claims = validate_token(
732///     "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
733///     &jwks,
734///     &validation,
735/// )?;
736/// ```
737///
738/// # Errors
739///
740/// Returns an error if:
741/// - The token format is invalid
742/// - No matching key is found in the JWKS
743/// - The signature verification fails
744/// - Claims validation fails
745#[cfg(feature = "jwt")]
746pub fn validate_token(
747    token: &str,
748    jwks: &JwksSet,
749    validation: &TokenValidation,
750) -> Result<TokenClaims, JwtError> {
751    // Decode header to get algorithm and key ID
752    let header = decode_header(token)?;
753
754    // Enforce the relying party's algorithm allowlist before trusting the
755    // header's `alg` (RFC 8725 §3.1). An empty allowlist accepts any supported
756    // algorithm.
757    if !validation.allowed_algorithms.is_empty()
758        && !validation
759            .allowed_algorithms
760            .iter()
761            .any(|a| a == &header.alg)
762    {
763        return Err(JwtError::AlgorithmNotAllowed {
764            algorithm: header.alg,
765        });
766    }
767
768    // Parse algorithm
769    let alg = JwtAlgorithm::parse(&header.alg).ok_or_else(|| JwtError::UnsupportedAlgorithm {
770        algorithm: header.alg.clone(),
771    })?;
772
773    // Find the matching key in JWKS
774    let jwk = if let Some(ref kid) = header.kid {
775        // If kid is specified, find that specific key
776        jwks.find_key(kid)
777            .ok_or_else(|| JwtError::NoMatchingKey { kid: kid.clone() })?
778    } else {
779        // No kid specified, try the first signing key with matching algorithm
780        jwks.keys
781            .iter()
782            .find(|k| k.is_signing_key() && k.alg.as_deref() == Some(&header.alg))
783            .or_else(|| jwks.first_key())
784            .ok_or_else(|| JwtError::NoMatchingKey {
785                kid: "<no kid specified>".to_string(),
786            })?
787    };
788
789    // Create decoding key from JWK
790    let decoding_key = create_decoding_key(jwk, alg)?;
791
792    // Build validation configuration
793    let jwt_validation = build_jwt_validation(validation, alg);
794
795    // Decode and verify the token
796    let token_data = jsonwebtoken::decode::<TokenClaims>(token, &decoding_key, &jwt_validation)
797        .map_err(|e| match e.kind() {
798            jsonwebtoken::errors::ErrorKind::ExpiredSignature => JwtError::Expired,
799            jsonwebtoken::errors::ErrorKind::ImmatureSignature => JwtError::NotYetValid,
800            jsonwebtoken::errors::ErrorKind::InvalidSignature => JwtError::InvalidSignature {
801                message: "signature verification failed".to_string(),
802            },
803            jsonwebtoken::errors::ErrorKind::InvalidAudience => JwtError::InvalidAudience {
804                expected: validation.audience.clone().unwrap_or_default(),
805            },
806            jsonwebtoken::errors::ErrorKind::InvalidIssuer => JwtError::InvalidIssuer {
807                expected: validation.issuer.clone().unwrap_or_default(),
808                actual: "<unknown>".to_string(),
809            },
810            _ => JwtError::InvalidFormat {
811                message: format!("token validation failed: {e}"),
812            },
813        })?;
814
815    // Additional validation jsonwebtoken doesn't handle: required scopes and
816    // required custom claims.
817    let claims = token_data.claims;
818    for required_scope in &validation.required_scopes {
819        if !claims.has_scope(required_scope) {
820            return Err(JwtError::InsufficientScope {
821                required: required_scope.clone(),
822            });
823        }
824    }
825    check_required_claims(&claims, &validation.required_claims)?;
826
827    Ok(claims)
828}
829
830/// Fetch JWKS from an authorization server's JWKS endpoint.
831///
832/// This function makes an HTTP GET request to the JWKS URI and parses the
833/// response as a JSON Web Key Set.
834///
835/// # Arguments
836///
837/// * `jwks_uri` - The URL of the JWKS endpoint (typically from authorization
838///   server metadata's `jwks_uri` field)
839///
840/// # Example
841///
842/// ```rust,ignore
843/// use mcpkit_core::auth::jwt::fetch_jwks;
844///
845/// let jwks = fetch_jwks("https://auth.example.com/.well-known/jwks.json").await?;
846/// if let Some(key) = jwks.find_key("my-key-id") {
847///     println!("Found key: {:?}", key);
848/// }
849/// ```
850///
851/// # Errors
852///
853/// Returns `JwtError::JwksFetchError` if:
854/// - The URI is not an `https://` URL
855/// - The HTTP request fails
856/// - The response cannot be parsed as a JWKS
857#[cfg(feature = "jwt")]
858pub async fn fetch_jwks(jwks_uri: &str) -> Result<JwksSet, JwtError> {
859    // Refuse plaintext transport: a JWKS fetched over HTTP can be tampered with
860    // in transit, letting an attacker swap in their own signing keys.
861    if !jwks_uri.starts_with("https://") {
862        return Err(JwtError::JwksFetchError {
863            message: format!("JWKS URI must use https://, got: {jwks_uri}"),
864        });
865    }
866
867    let response = reqwest::get(jwks_uri)
868        .await
869        .map_err(|e| JwtError::JwksFetchError {
870            message: format!("HTTP request failed: {e}"),
871        })?;
872
873    if !response.status().is_success() {
874        return Err(JwtError::JwksFetchError {
875            message: format!("HTTP {} from JWKS endpoint", response.status()),
876        });
877    }
878
879    response.json().await.map_err(|e| JwtError::JwksFetchError {
880        message: format!("Failed to parse JWKS response: {e}"),
881    })
882}
883
884/// Validate a JWT access token with full signature verification by fetching
885/// JWKS from the authorization server.
886///
887/// This is a convenience function that:
888/// 1. Fetches the JWKS from the authorization server
889/// 2. Verifies the token's cryptographic signature using the public keys
890/// 3. Validates the claims against the validation configuration
891///
892/// # Arguments
893///
894/// * `token` - The JWT access token to validate
895/// * `jwks_uri` - The URL of the JWKS endpoint
896/// * `validation` - The validation configuration
897///
898/// # Example
899///
900/// ```rust,ignore
901/// use mcpkit_core::auth::jwt::{validate_token_with_fetch, TokenValidation};
902///
903/// let validation = TokenValidation::new()
904///     .with_issuer("https://auth.example.com")
905///     .with_audience("https://mcp.example.com");
906///
907/// let claims = validate_token_with_fetch(
908///     "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
909///     "https://auth.example.com/.well-known/jwks.json",
910///     &validation,
911/// ).await?;
912///
913/// println!("Token subject: {:?}", claims.sub);
914/// ```
915///
916/// # Security
917///
918/// This function performs full cryptographic signature verification using
919/// the public keys from the JWKS endpoint. It supports RS256, RS384, RS512,
920/// ES256, and ES384 algorithms.
921///
922/// For production use, consider implementing JWKS caching to avoid fetching
923/// keys on every token validation.
924///
925/// # Errors
926///
927/// Returns an error if:
928/// - JWKS fetching fails
929/// - No matching key is found in the JWKS
930/// - Signature verification fails
931/// - Claims validation fails
932#[cfg(feature = "jwt")]
933pub async fn validate_token_with_fetch(
934    token: &str,
935    jwks_uri: &str,
936    validation: &TokenValidation,
937) -> Result<TokenClaims, JwtError> {
938    // Fetch JWKS from the authorization server
939    let jwks = fetch_jwks(jwks_uri).await?;
940
941    // Validate token with signature verification
942    validate_token(token, &jwks, validation)
943}
944
945#[cfg(test)]
946mod tests {
947    use super::*;
948
949    #[test]
950    fn test_token_validation_builder() {
951        let validation = TokenValidation::new()
952            .with_issuer("https://auth.example.com")
953            .with_audience("https://mcp.example.com")
954            .with_required_scope("mcp:read")
955            .with_leeway(120);
956
957        assert_eq!(
958            validation.issuer,
959            Some("https://auth.example.com".to_string())
960        );
961        assert_eq!(
962            validation.audience,
963            Some("https://mcp.example.com".to_string())
964        );
965        assert_eq!(validation.required_scopes, vec!["mcp:read"]);
966        assert_eq!(validation.leeway_seconds, 120);
967    }
968
969    #[test]
970    fn test_audience_contains() {
971        let single = Audience::Single("api".to_string());
972        assert!(single.contains("api"));
973        assert!(!single.contains("other"));
974
975        let multiple = Audience::Multiple(vec!["api".to_string(), "web".to_string()]);
976        assert!(multiple.contains("api"));
977        assert!(multiple.contains("web"));
978        assert!(!multiple.contains("other"));
979    }
980
981    #[test]
982    fn test_token_claims_scopes() {
983        let claims = TokenClaims {
984            iss: None,
985            sub: None,
986            aud: None,
987            exp: None,
988            iat: None,
989            nbf: None,
990            jti: None,
991            scope: Some("mcp:read mcp:write".to_string()),
992            extra: HashMap::new(),
993        };
994
995        assert!(claims.has_scope("mcp:read"));
996        assert!(claims.has_scope("mcp:write"));
997        assert!(!claims.has_scope("mcp:admin"));
998
999        let scopes = claims.scopes();
1000        assert_eq!(scopes, vec!["mcp:read", "mcp:write"]);
1001    }
1002
1003    #[test]
1004    fn test_decode_header() {
1005        // A minimal JWT header: {"alg":"RS256","typ":"JWT","kid":"key-1"}
1006        let token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtleS0xIn0.e30.sig";
1007        let header = decode_header(token).unwrap();
1008
1009        assert_eq!(header.alg, "RS256");
1010        assert_eq!(header.typ, Some("JWT".to_string()));
1011        assert_eq!(header.kid, Some("key-1".to_string()));
1012    }
1013
1014    #[test]
1015    fn test_decode_header_invalid() {
1016        assert!(decode_header("invalid").is_err());
1017        assert!(decode_header("a.b").is_err());
1018        assert!(decode_header("").is_err());
1019    }
1020
1021    #[test]
1022    fn test_decode_claims_unverified() {
1023        // JWT with claims: {"iss":"test","sub":"user123","exp":9999999999}
1024        let token = "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ0ZXN0Iiwic3ViIjoidXNlcjEyMyIsImV4cCI6OTk5OTk5OTk5OX0.sig";
1025        let claims = decode_claims_unverified(token).unwrap();
1026
1027        assert_eq!(claims.iss, Some("test".to_string()));
1028        assert_eq!(claims.sub, Some("user123".to_string()));
1029        assert_eq!(claims.exp, Some(9_999_999_999));
1030    }
1031
1032    #[test]
1033    fn test_validate_claims_expired() {
1034        let claims = TokenClaims {
1035            iss: None,
1036            sub: None,
1037            aud: None,
1038            exp: Some(1000), // Long expired
1039            iat: None,
1040            nbf: None,
1041            jti: None,
1042            scope: None,
1043            extra: HashMap::new(),
1044        };
1045
1046        let validation = TokenValidation::new();
1047        let result = validate_claims(&claims, &validation);
1048        assert!(matches!(result, Err(JwtError::Expired)));
1049    }
1050
1051    #[test]
1052    fn test_validate_claims_wrong_issuer() {
1053        let claims = TokenClaims {
1054            iss: Some("wrong-issuer".to_string()),
1055            sub: None,
1056            aud: None,
1057            exp: Some(u64::MAX),
1058            iat: None,
1059            nbf: None,
1060            jti: None,
1061            scope: None,
1062            extra: HashMap::new(),
1063        };
1064
1065        let validation = TokenValidation::new().with_issuer("expected-issuer");
1066        let result = validate_claims(&claims, &validation);
1067        assert!(matches!(result, Err(JwtError::InvalidIssuer { .. })));
1068    }
1069
1070    #[test]
1071    fn test_validate_claims_insufficient_scope() {
1072        let claims = TokenClaims {
1073            iss: None,
1074            sub: None,
1075            aud: None,
1076            exp: Some(u64::MAX),
1077            iat: None,
1078            nbf: None,
1079            jti: None,
1080            scope: Some("mcp:read".to_string()),
1081            extra: HashMap::new(),
1082        };
1083
1084        let validation = TokenValidation::new()
1085            .without_exp_validation()
1086            .with_required_scope("mcp:admin");
1087        let result = validate_claims(&claims, &validation);
1088        assert!(matches!(result, Err(JwtError::InsufficientScope { .. })));
1089    }
1090
1091    #[test]
1092    fn test_validate_claims_success() {
1093        let claims = TokenClaims {
1094            iss: Some("https://auth.example.com".to_string()),
1095            sub: Some("user123".to_string()),
1096            aud: Some(Audience::Single("https://mcp.example.com".to_string())),
1097            exp: Some(u64::MAX),
1098            iat: Some(1000),
1099            nbf: None,
1100            jti: None,
1101            scope: Some("mcp:read mcp:write".to_string()),
1102            extra: HashMap::new(),
1103        };
1104
1105        let validation = TokenValidation::new()
1106            .with_issuer("https://auth.example.com")
1107            .with_audience("https://mcp.example.com")
1108            .with_required_scope("mcp:read");
1109
1110        let result = validate_claims(&claims, &validation);
1111        assert!(result.is_ok());
1112    }
1113
1114    #[test]
1115    fn test_jwk_type_detection() {
1116        let rsa_key = Jwk {
1117            kty: "RSA".to_string(),
1118            kid: Some("rsa-key".to_string()),
1119            key_use: Some("sig".to_string()),
1120            alg: Some("RS256".to_string()),
1121            n: Some("...".to_string()),
1122            e: Some("AQAB".to_string()),
1123            crv: None,
1124            x: None,
1125            y: None,
1126        };
1127
1128        assert!(rsa_key.is_rsa());
1129        assert!(!rsa_key.is_ec());
1130        assert!(rsa_key.is_signing_key());
1131
1132        let ec_key = Jwk {
1133            kty: "EC".to_string(),
1134            kid: Some("ec-key".to_string()),
1135            key_use: Some("sig".to_string()),
1136            alg: Some("ES256".to_string()),
1137            n: None,
1138            e: None,
1139            crv: Some("P-256".to_string()),
1140            x: Some("...".to_string()),
1141            y: Some("...".to_string()),
1142        };
1143
1144        assert!(!ec_key.is_rsa());
1145        assert!(ec_key.is_ec());
1146        assert!(ec_key.is_signing_key());
1147    }
1148
1149    #[test]
1150    fn test_jwks_find_key() {
1151        let jwks = JwksSet {
1152            keys: vec![
1153                Jwk {
1154                    kty: "RSA".to_string(),
1155                    kid: Some("key-1".to_string()),
1156                    key_use: Some("sig".to_string()),
1157                    alg: Some("RS256".to_string()),
1158                    n: Some("...".to_string()),
1159                    e: Some("AQAB".to_string()),
1160                    crv: None,
1161                    x: None,
1162                    y: None,
1163                },
1164                Jwk {
1165                    kty: "RSA".to_string(),
1166                    kid: Some("key-2".to_string()),
1167                    key_use: Some("sig".to_string()),
1168                    alg: Some("RS256".to_string()),
1169                    n: Some("...".to_string()),
1170                    e: Some("AQAB".to_string()),
1171                    crv: None,
1172                    x: None,
1173                    y: None,
1174                },
1175            ],
1176        };
1177
1178        let key = jwks.find_key("key-1");
1179        assert!(key.is_some());
1180        assert_eq!(key.unwrap().kid, Some("key-1".to_string()));
1181
1182        let key = jwks.find_key("key-2");
1183        assert!(key.is_some());
1184
1185        let key = jwks.find_key("nonexistent");
1186        assert!(key.is_none());
1187
1188        let first = jwks.first_key();
1189        assert!(first.is_some());
1190    }
1191
1192    #[test]
1193    fn test_jwt_error_display() {
1194        let err = JwtError::Expired;
1195        assert_eq!(err.to_string(), "token expired");
1196
1197        let err = JwtError::InvalidIssuer {
1198            expected: "a".to_string(),
1199            actual: "b".to_string(),
1200        };
1201        assert!(err.to_string().contains("expected a"));
1202        assert!(err.to_string().contains("got b"));
1203    }
1204}
1205
1206/// Tests that require the `jwt` feature for signature verification.
1207#[cfg(all(test, feature = "jwt"))]
1208mod signature_tests {
1209    use super::*;
1210    use rsa::pkcs8::EncodePrivateKey;
1211    use rsa::traits::PublicKeyParts;
1212
1213    /// Create a signed JWT for testing using jsonwebtoken's encode function.
1214    fn create_test_jwt(
1215        alg: jsonwebtoken::Algorithm,
1216        encoding_key: &jsonwebtoken::EncodingKey,
1217        kid: Option<&str>,
1218        claims: &TokenClaims,
1219    ) -> String {
1220        let mut header = jsonwebtoken::Header::new(alg);
1221        header.kid = kid.map(String::from);
1222        jsonwebtoken::encode(&header, claims, encoding_key).expect("failed to encode JWT")
1223    }
1224
1225    /// Create test claims with far-future expiration.
1226    fn make_test_claims() -> TokenClaims {
1227        TokenClaims {
1228            iss: Some("https://auth.example.com".to_string()),
1229            sub: Some("user123".to_string()),
1230            aud: Some(Audience::Single("https://mcp.example.com".to_string())),
1231            exp: Some(u64::MAX / 2), // Far future but not overflow
1232            iat: Some(1_000_000),
1233            nbf: None,
1234            jti: Some("test-jti-123".to_string()),
1235            scope: Some("mcp:read mcp:write".to_string()),
1236            extra: HashMap::new(),
1237        }
1238    }
1239
1240    #[test]
1241    fn test_jwt_algorithm_parse() {
1242        assert_eq!(JwtAlgorithm::parse("RS256"), Some(JwtAlgorithm::RS256));
1243        assert_eq!(JwtAlgorithm::parse("RS384"), Some(JwtAlgorithm::RS384));
1244        assert_eq!(JwtAlgorithm::parse("RS512"), Some(JwtAlgorithm::RS512));
1245        assert_eq!(JwtAlgorithm::parse("ES256"), Some(JwtAlgorithm::ES256));
1246        assert_eq!(JwtAlgorithm::parse("ES384"), Some(JwtAlgorithm::ES384));
1247        assert_eq!(JwtAlgorithm::parse("HS256"), None);
1248        assert_eq!(JwtAlgorithm::parse("invalid"), None);
1249    }
1250
1251    #[test]
1252    fn test_jwt_algorithm_display() {
1253        assert_eq!(JwtAlgorithm::RS256.to_string(), "RS256");
1254        assert_eq!(JwtAlgorithm::ES256.to_string(), "ES256");
1255        assert_eq!(JwtAlgorithm::RS512.to_string(), "RS512");
1256    }
1257
1258    #[test]
1259    fn test_validate_token_rs256() {
1260        // Generate RSA key pair for testing
1261        use base64::Engine;
1262        use rand::rngs::OsRng;
1263        use rsa::RsaPrivateKey;
1264
1265        let mut rng = OsRng;
1266        let private_key = RsaPrivateKey::new(&mut rng, 2048).expect("failed to generate key");
1267        let public_key = private_key.to_public_key();
1268
1269        // Get modulus and exponent for JWKS
1270        let n_bytes = public_key.n().to_bytes_be();
1271        let e_bytes = public_key.e().to_bytes_be();
1272        let n = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&n_bytes);
1273        let e = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&e_bytes);
1274
1275        // Create JWKS with the public key
1276        let jwks = JwksSet {
1277            keys: vec![Jwk {
1278                kty: "RSA".to_string(),
1279                kid: Some("test-key-1".to_string()),
1280                key_use: Some("sig".to_string()),
1281                alg: Some("RS256".to_string()),
1282                n: Some(n),
1283                e: Some(e),
1284                crv: None,
1285                x: None,
1286                y: None,
1287            }],
1288        };
1289
1290        // Create encoding key from private key
1291        let pem = private_key
1292            .to_pkcs8_pem(rsa::pkcs8::LineEnding::LF)
1293            .expect("failed to encode private key");
1294        let encoding_key = jsonwebtoken::EncodingKey::from_rsa_pem(pem.as_bytes())
1295            .expect("failed to create encoding key");
1296
1297        // Create and sign a token
1298        let claims = make_test_claims();
1299        let token = create_test_jwt(
1300            jsonwebtoken::Algorithm::RS256,
1301            &encoding_key,
1302            Some("test-key-1"),
1303            &claims,
1304        );
1305
1306        // Validate the token
1307        let validation = TokenValidation::new()
1308            .with_issuer("https://auth.example.com")
1309            .with_audience("https://mcp.example.com");
1310
1311        let result = validate_token(&token, &jwks, &validation);
1312        assert!(result.is_ok(), "validation failed: {:?}", result.err());
1313
1314        let validated_claims = result.unwrap();
1315        assert_eq!(validated_claims.sub, Some("user123".to_string()));
1316        assert_eq!(
1317            validated_claims.iss,
1318            Some("https://auth.example.com".to_string())
1319        );
1320    }
1321
1322    #[test]
1323    fn test_validate_token_algorithm_allowlist() {
1324        use base64::Engine;
1325        use rand::rngs::OsRng;
1326        use rsa::RsaPrivateKey;
1327
1328        let mut rng = OsRng;
1329        let private_key = RsaPrivateKey::new(&mut rng, 2048).expect("failed to generate key");
1330        let public_key = private_key.to_public_key();
1331        let n =
1332            base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(public_key.n().to_bytes_be());
1333        let e =
1334            base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(public_key.e().to_bytes_be());
1335
1336        let jwks = JwksSet {
1337            keys: vec![Jwk {
1338                kty: "RSA".to_string(),
1339                kid: Some("test-key-1".to_string()),
1340                key_use: Some("sig".to_string()),
1341                alg: Some("RS256".to_string()),
1342                n: Some(n),
1343                e: Some(e),
1344                crv: None,
1345                x: None,
1346                y: None,
1347            }],
1348        };
1349
1350        let pem = private_key
1351            .to_pkcs8_pem(rsa::pkcs8::LineEnding::LF)
1352            .expect("failed to encode private key");
1353        let encoding_key = jsonwebtoken::EncodingKey::from_rsa_pem(pem.as_bytes())
1354            .expect("failed to create encoding key");
1355        let token = create_test_jwt(
1356            jsonwebtoken::Algorithm::RS256,
1357            &encoding_key,
1358            Some("test-key-1"),
1359            &make_test_claims(),
1360        );
1361
1362        // Allowlist contains the token's algorithm: accepted.
1363        let allowed = TokenValidation::new()
1364            .with_issuer("https://auth.example.com")
1365            .with_audience("https://mcp.example.com")
1366            .with_allowed_algorithms(["RS256"]);
1367        assert!(validate_token(&token, &jwks, &allowed).is_ok());
1368
1369        // Allowlist excludes the token's algorithm: rejected up front.
1370        let disallowed = TokenValidation::new()
1371            .with_issuer("https://auth.example.com")
1372            .with_audience("https://mcp.example.com")
1373            .with_allowed_algorithms(["ES256"]);
1374        assert!(matches!(
1375            validate_token(&token, &jwks, &disallowed),
1376            Err(JwtError::AlgorithmNotAllowed { .. })
1377        ));
1378    }
1379
1380    #[tokio::test]
1381    async fn test_fetch_jwks_rejects_non_https() {
1382        let err = fetch_jwks("http://auth.example.com/.well-known/jwks.json")
1383            .await
1384            .expect_err("non-https JWKS URI must be rejected");
1385        assert!(matches!(err, JwtError::JwksFetchError { .. }));
1386    }
1387
1388    #[test]
1389    fn test_validate_token_es256() {
1390        use base64::Engine;
1391        use p256::ecdsa::{SigningKey, VerifyingKey};
1392        use p256::elliptic_curve::sec1::ToEncodedPoint;
1393        use p256::pkcs8::EncodePrivateKey as EcEncodePrivateKey;
1394        use rand::rngs::OsRng;
1395
1396        // Generate EC P-256 key pair
1397        let signing_key = SigningKey::random(&mut OsRng);
1398        let verifying_key = VerifyingKey::from(&signing_key);
1399
1400        // Get x and y coordinates for JWKS (uncompressed point)
1401        let point = verifying_key.as_affine().to_encoded_point(false);
1402        let x_bytes = point.x().expect("x coordinate");
1403        let y_bytes = point.y().expect("y coordinate");
1404        let x = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(x_bytes);
1405        let y = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(y_bytes);
1406
1407        // Create JWKS with the public key
1408        let jwks = JwksSet {
1409            keys: vec![Jwk {
1410                kty: "EC".to_string(),
1411                kid: Some("test-ec-key-1".to_string()),
1412                key_use: Some("sig".to_string()),
1413                alg: Some("ES256".to_string()),
1414                n: None,
1415                e: None,
1416                crv: Some("P-256".to_string()),
1417                x: Some(x),
1418                y: Some(y),
1419            }],
1420        };
1421
1422        // Create encoding key from private key
1423        let pkcs8_der = signing_key.to_pkcs8_der().expect("failed to encode EC key");
1424        let encoding_key = jsonwebtoken::EncodingKey::from_ec_der(pkcs8_der.as_bytes());
1425
1426        // Create and sign a token
1427        let claims = make_test_claims();
1428        let token = create_test_jwt(
1429            jsonwebtoken::Algorithm::ES256,
1430            &encoding_key,
1431            Some("test-ec-key-1"),
1432            &claims,
1433        );
1434
1435        // Validate the token
1436        let validation = TokenValidation::new()
1437            .with_issuer("https://auth.example.com")
1438            .with_audience("https://mcp.example.com");
1439
1440        let result = validate_token(&token, &jwks, &validation);
1441        assert!(
1442            result.is_ok(),
1443            "ES256 validation failed: {:?}",
1444            result.err()
1445        );
1446
1447        let validated_claims = result.unwrap();
1448        assert_eq!(validated_claims.sub, Some("user123".to_string()));
1449    }
1450
1451    /// Regression test for #10: custom required claims must actually be
1452    /// enforced by the signature-verifying path (the old code delegated to
1453    /// `set_required_spec_claims`, which ignores non-registered claims and
1454    /// dropped the default `exp` requirement).
1455    #[test]
1456    fn test_validate_token_enforces_custom_required_claims() {
1457        use base64::Engine;
1458        use p256::ecdsa::{SigningKey, VerifyingKey};
1459        use p256::elliptic_curve::sec1::ToEncodedPoint;
1460        use p256::pkcs8::EncodePrivateKey as EcEncodePrivateKey;
1461        use rand::rngs::OsRng;
1462
1463        let signing_key = SigningKey::random(&mut OsRng);
1464        let verifying_key = VerifyingKey::from(&signing_key);
1465        let point = verifying_key.as_affine().to_encoded_point(false);
1466        let x = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(point.x().expect("x"));
1467        let y = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(point.y().expect("y"));
1468
1469        let jwks = JwksSet {
1470            keys: vec![Jwk {
1471                kty: "EC".to_string(),
1472                kid: Some("ec-1".to_string()),
1473                key_use: Some("sig".to_string()),
1474                alg: Some("ES256".to_string()),
1475                n: None,
1476                e: None,
1477                crv: Some("P-256".to_string()),
1478                x: Some(x),
1479                y: Some(y),
1480            }],
1481        };
1482        let pkcs8 = signing_key.to_pkcs8_der().expect("encode EC key");
1483        let encoding_key = jsonwebtoken::EncodingKey::from_ec_der(pkcs8.as_bytes());
1484
1485        let validation = TokenValidation::new()
1486            .with_issuer("https://auth.example.com")
1487            .with_audience("https://mcp.example.com")
1488            .with_required_claim("tenant_id");
1489
1490        // A signed, otherwise-valid token WITHOUT the custom claim must be
1491        // rejected (the old code accepted it).
1492        let token = create_test_jwt(
1493            jsonwebtoken::Algorithm::ES256,
1494            &encoding_key,
1495            Some("ec-1"),
1496            &make_test_claims(),
1497        );
1498        match validate_token(&token, &jwks, &validation) {
1499            Err(JwtError::MissingClaim { claim }) => assert_eq!(claim.as_str(), "tenant_id"),
1500            other => panic!("expected MissingClaim(tenant_id), got {other:?}"),
1501        }
1502
1503        // The same token WITH the custom claim present is accepted.
1504        let mut claims = make_test_claims();
1505        claims
1506            .extra
1507            .insert("tenant_id".to_string(), serde_json::json!("acme"));
1508        let token = create_test_jwt(
1509            jsonwebtoken::Algorithm::ES256,
1510            &encoding_key,
1511            Some("ec-1"),
1512            &claims,
1513        );
1514        assert!(
1515            validate_token(&token, &jwks, &validation).is_ok(),
1516            "token with the required custom claim should validate"
1517        );
1518
1519        // Configuring a custom required claim must NOT drop the default
1520        // exp-presence requirement: a token with the claim but no `exp` is
1521        // still rejected.
1522        let mut claims = make_test_claims();
1523        claims
1524            .extra
1525            .insert("tenant_id".to_string(), serde_json::json!("acme"));
1526        claims.exp = None;
1527        let token = create_test_jwt(
1528            jsonwebtoken::Algorithm::ES256,
1529            &encoding_key,
1530            Some("ec-1"),
1531            &claims,
1532        );
1533        assert!(
1534            validate_token(&token, &jwks, &validation).is_err(),
1535            "missing exp must still be rejected when required_claims is set"
1536        );
1537    }
1538
1539    #[test]
1540    fn test_validate_token_invalid_signature() {
1541        use base64::Engine;
1542        use rand::rngs::OsRng;
1543        use rsa::RsaPrivateKey;
1544
1545        let mut rng = OsRng;
1546
1547        // Generate two different key pairs
1548        let private_key_1 = RsaPrivateKey::new(&mut rng, 2048).expect("failed to generate key 1");
1549        let private_key_2 = RsaPrivateKey::new(&mut rng, 2048).expect("failed to generate key 2");
1550        let public_key_2 = private_key_2.to_public_key();
1551
1552        // Create JWKS with public key from key pair 2
1553        let n_bytes = public_key_2.n().to_bytes_be();
1554        let e_bytes = public_key_2.e().to_bytes_be();
1555        let n = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&n_bytes);
1556        let e = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&e_bytes);
1557
1558        let jwks = JwksSet {
1559            keys: vec![Jwk {
1560                kty: "RSA".to_string(),
1561                kid: Some("key-2".to_string()),
1562                key_use: Some("sig".to_string()),
1563                alg: Some("RS256".to_string()),
1564                n: Some(n),
1565                e: Some(e),
1566                crv: None,
1567                x: None,
1568                y: None,
1569            }],
1570        };
1571
1572        // Sign token with key pair 1 (different from JWKS)
1573        let pem_1 = private_key_1
1574            .to_pkcs8_pem(rsa::pkcs8::LineEnding::LF)
1575            .expect("failed to encode private key");
1576        let encoding_key_1 = jsonwebtoken::EncodingKey::from_rsa_pem(pem_1.as_bytes())
1577            .expect("failed to create encoding key");
1578
1579        let claims = make_test_claims();
1580        let token = create_test_jwt(
1581            jsonwebtoken::Algorithm::RS256,
1582            &encoding_key_1,
1583            Some("key-2"), // Use kid from JWKS but signed with different key
1584            &claims,
1585        );
1586
1587        // Validation should fail due to signature mismatch
1588        let validation = TokenValidation::new()
1589            .with_issuer("https://auth.example.com")
1590            .with_audience("https://mcp.example.com");
1591
1592        let result = validate_token(&token, &jwks, &validation);
1593        assert!(result.is_err());
1594        assert!(matches!(
1595            result.unwrap_err(),
1596            JwtError::InvalidSignature { .. }
1597        ));
1598    }
1599
1600    #[test]
1601    fn test_validate_token_no_matching_key() {
1602        // Create JWKS with a key that has a different kid
1603        let jwks = JwksSet {
1604            keys: vec![Jwk {
1605                kty: "RSA".to_string(),
1606                kid: Some("different-key".to_string()),
1607                key_use: Some("sig".to_string()),
1608                alg: Some("RS256".to_string()),
1609                n: Some("test".to_string()),
1610                e: Some("AQAB".to_string()),
1611                crv: None,
1612                x: None,
1613                y: None,
1614            }],
1615        };
1616
1617        // Create a token with header specifying kid that doesn't exist in JWKS
1618        // Header: {"alg":"RS256","kid":"nonexistent-key"}
1619        let token = "eyJhbGciOiJSUzI1NiIsImtpZCI6Im5vbmV4aXN0ZW50LWtleSJ9.eyJpc3MiOiJ0ZXN0In0.sig";
1620
1621        let validation = TokenValidation::new();
1622        let result = validate_token(token, &jwks, &validation);
1623
1624        assert!(result.is_err());
1625        assert!(matches!(
1626            result.unwrap_err(),
1627            JwtError::NoMatchingKey { .. }
1628        ));
1629    }
1630
1631    #[test]
1632    fn test_validate_token_unsupported_algorithm() {
1633        let jwks = JwksSet { keys: vec![] };
1634
1635        // Token with HS256 algorithm (not supported)
1636        // Header: {"alg":"HS256"}
1637        let token = "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0In0.sig";
1638
1639        let validation = TokenValidation::new();
1640        let result = validate_token(token, &jwks, &validation);
1641
1642        assert!(result.is_err());
1643        assert!(matches!(
1644            result.unwrap_err(),
1645            JwtError::UnsupportedAlgorithm { .. }
1646        ));
1647    }
1648
1649    #[test]
1650    fn test_validate_token_scope_validation() {
1651        use base64::Engine;
1652        use rand::rngs::OsRng;
1653        use rsa::RsaPrivateKey;
1654
1655        let mut rng = OsRng;
1656        let private_key = RsaPrivateKey::new(&mut rng, 2048).expect("failed to generate key");
1657        let public_key = private_key.to_public_key();
1658
1659        let n_bytes = public_key.n().to_bytes_be();
1660        let e_bytes = public_key.e().to_bytes_be();
1661        let n = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&n_bytes);
1662        let e = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&e_bytes);
1663
1664        let jwks = JwksSet {
1665            keys: vec![Jwk {
1666                kty: "RSA".to_string(),
1667                kid: Some("test-key".to_string()),
1668                key_use: Some("sig".to_string()),
1669                alg: Some("RS256".to_string()),
1670                n: Some(n),
1671                e: Some(e),
1672                crv: None,
1673                x: None,
1674                y: None,
1675            }],
1676        };
1677
1678        let pem = private_key
1679            .to_pkcs8_pem(rsa::pkcs8::LineEnding::LF)
1680            .expect("failed to encode private key");
1681        let encoding_key = jsonwebtoken::EncodingKey::from_rsa_pem(pem.as_bytes())
1682            .expect("failed to create encoding key");
1683
1684        // Create claims with limited scope
1685        let mut claims = make_test_claims();
1686        claims.scope = Some("mcp:read".to_string()); // Only read scope
1687
1688        let token = create_test_jwt(
1689            jsonwebtoken::Algorithm::RS256,
1690            &encoding_key,
1691            Some("test-key"),
1692            &claims,
1693        );
1694
1695        // Request a scope that isn't present
1696        let validation = TokenValidation::new()
1697            .with_issuer("https://auth.example.com")
1698            .with_audience("https://mcp.example.com")
1699            .with_required_scope("mcp:admin"); // Not in token
1700
1701        let result = validate_token(&token, &jwks, &validation);
1702        assert!(result.is_err());
1703        assert!(matches!(
1704            result.unwrap_err(),
1705            JwtError::InsufficientScope { .. }
1706        ));
1707    }
1708
1709    #[test]
1710    fn test_validate_token_expired() {
1711        use base64::Engine;
1712        use rand::rngs::OsRng;
1713        use rsa::RsaPrivateKey;
1714
1715        let mut rng = OsRng;
1716        let private_key = RsaPrivateKey::new(&mut rng, 2048).expect("failed to generate key");
1717        let public_key = private_key.to_public_key();
1718
1719        let n_bytes = public_key.n().to_bytes_be();
1720        let e_bytes = public_key.e().to_bytes_be();
1721        let n = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&n_bytes);
1722        let e = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&e_bytes);
1723
1724        let jwks = JwksSet {
1725            keys: vec![Jwk {
1726                kty: "RSA".to_string(),
1727                kid: Some("test-key".to_string()),
1728                key_use: Some("sig".to_string()),
1729                alg: Some("RS256".to_string()),
1730                n: Some(n),
1731                e: Some(e),
1732                crv: None,
1733                x: None,
1734                y: None,
1735            }],
1736        };
1737
1738        let pem = private_key
1739            .to_pkcs8_pem(rsa::pkcs8::LineEnding::LF)
1740            .expect("failed to encode private key");
1741        let encoding_key = jsonwebtoken::EncodingKey::from_rsa_pem(pem.as_bytes())
1742            .expect("failed to create encoding key");
1743
1744        // Create expired claims
1745        let mut claims = make_test_claims();
1746        claims.exp = Some(1000); // Expired long ago
1747
1748        let token = create_test_jwt(
1749            jsonwebtoken::Algorithm::RS256,
1750            &encoding_key,
1751            Some("test-key"),
1752            &claims,
1753        );
1754
1755        let validation = TokenValidation::new()
1756            .with_issuer("https://auth.example.com")
1757            .with_audience("https://mcp.example.com");
1758
1759        let result = validate_token(&token, &jwks, &validation);
1760        assert!(result.is_err());
1761        assert!(matches!(result.unwrap_err(), JwtError::Expired));
1762    }
1763
1764    #[test]
1765    fn test_validate_token_key_selection_by_algorithm() {
1766        use base64::Engine;
1767        use rand::rngs::OsRng;
1768        use rsa::RsaPrivateKey;
1769
1770        let mut rng = OsRng;
1771        let private_key = RsaPrivateKey::new(&mut rng, 2048).expect("failed to generate key");
1772        let public_key = private_key.to_public_key();
1773
1774        let n_bytes = public_key.n().to_bytes_be();
1775        let e_bytes = public_key.e().to_bytes_be();
1776        let n = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&n_bytes);
1777        let e = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&e_bytes);
1778
1779        // JWKS with multiple keys - one without kid
1780        let jwks = JwksSet {
1781            keys: vec![Jwk {
1782                kty: "RSA".to_string(),
1783                kid: None, // No kid
1784                key_use: Some("sig".to_string()),
1785                alg: Some("RS256".to_string()),
1786                n: Some(n),
1787                e: Some(e),
1788                crv: None,
1789                x: None,
1790                y: None,
1791            }],
1792        };
1793
1794        let pem = private_key
1795            .to_pkcs8_pem(rsa::pkcs8::LineEnding::LF)
1796            .expect("failed to encode private key");
1797        let encoding_key = jsonwebtoken::EncodingKey::from_rsa_pem(pem.as_bytes())
1798            .expect("failed to create encoding key");
1799
1800        // Create token without kid in header
1801        let claims = make_test_claims();
1802        let token = create_test_jwt(
1803            jsonwebtoken::Algorithm::RS256,
1804            &encoding_key,
1805            None, // No kid in token
1806            &claims,
1807        );
1808
1809        let validation = TokenValidation::new()
1810            .with_issuer("https://auth.example.com")
1811            .with_audience("https://mcp.example.com");
1812
1813        let result = validate_token(&token, &jwks, &validation);
1814        assert!(
1815            result.is_ok(),
1816            "key selection by algorithm failed: {:?}",
1817            result.err()
1818        );
1819    }
1820
1821    #[test]
1822    fn test_create_decoding_key_missing_rsa_components() {
1823        let incomplete_jwk = Jwk {
1824            kty: "RSA".to_string(),
1825            kid: Some("incomplete".to_string()),
1826            key_use: Some("sig".to_string()),
1827            alg: Some("RS256".to_string()),
1828            n: None, // Missing!
1829            e: Some("AQAB".to_string()),
1830            crv: None,
1831            x: None,
1832            y: None,
1833        };
1834
1835        let result = create_decoding_key(&incomplete_jwk, JwtAlgorithm::RS256);
1836        assert!(result.is_err());
1837        assert!(matches!(
1838            result.unwrap_err(),
1839            JwtError::InvalidFormat { .. }
1840        ));
1841    }
1842
1843    #[test]
1844    fn test_create_decoding_key_missing_ec_components() {
1845        let incomplete_jwk = Jwk {
1846            kty: "EC".to_string(),
1847            kid: Some("incomplete-ec".to_string()),
1848            key_use: Some("sig".to_string()),
1849            alg: Some("ES256".to_string()),
1850            n: None,
1851            e: None,
1852            crv: Some("P-256".to_string()),
1853            x: Some("test".to_string()),
1854            y: None, // Missing!
1855        };
1856
1857        let result = create_decoding_key(&incomplete_jwk, JwtAlgorithm::ES256);
1858        assert!(result.is_err());
1859        assert!(matches!(
1860            result.unwrap_err(),
1861            JwtError::InvalidFormat { .. }
1862        ));
1863    }
1864}