Skip to main content

fraiseql_core/security/
auth_middleware.rs

1//! Authentication Middleware
2//!
3//! This module provides authentication validation for GraphQL requests.
4//! It validates:
5//! - Authentication requirement (auth mandatory or optional)
6//! - JWT token extraction from Authorization header
7//! - Token signature verification (HS256/RS256/RS384/RS512)
8//! - Token expiry validation (exp claim)
9//! - Required claims validation (sub, exp, aud, iss)
10//!
11//! # Architecture
12//!
13//! The Auth middleware acts as the second layer in the security middleware:
14//! ```text
15//! HTTP Request with Authorization header
16//!     ↓
17//! AuthMiddleware::validate_request()
18//!     ├─ Check 1: Extract token from Authorization header
19//!     ├─ Check 2: Validate token structure and signature (HS256/RS256)
20//!     ├─ Check 3: Check token expiry (exp claim)
21//!     ├─ Check 4: Validate required claims (sub, exp)
22//!     └─ Check 5: Extract user info from claims
23//!     ↓
24//! Result<AuthenticatedUser> (user info or error)
25//! ```
26//!
27//! # Signature Verification
28//!
29//! The middleware supports multiple signing algorithms:
30//! - **HS256** (HMAC-SHA256): Symmetric key, good for internal services
31//! - **RS256/RS384/RS512** (RSA): Asymmetric key, good for external providers
32//!
33//! # Examples
34//!
35//! ```ignore
36//! use fraiseql_core::security::{AuthMiddleware, AuthConfig, SigningKey};
37//!
38//! // Create middleware with HS256 signing key
39//! let config = AuthConfig {
40//!     required: true,
41//!     token_expiry_secs: 3600,
42//!     signing_key: Some(SigningKey::hs256("your-secret-key")),
43//!     issuer: Some("https://your-issuer.com".to_string()),
44//!     audience: Some("your-api".to_string()),
45//! };
46//! let middleware = AuthMiddleware::from_config(config);
47//!
48//! // Validate a request (extract and validate token with signature verification)
49//! let user = middleware.validate_request(&request)?;
50//! println!("Authenticated user: {}", user.user_id);
51//! println!("Scopes: {:?}", user.scopes);
52//! println!("Expires: {}", user.expires_at);
53//! ```
54
55use std::fmt;
56
57use chrono::{DateTime, Utc};
58use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode};
59use serde::{Deserialize, Serialize};
60use zeroize::Zeroizing;
61
62use crate::security::errors::{Result, SecurityError};
63
64// ============================================================================
65// Signing Key Configuration
66// ============================================================================
67
68/// Signing key for JWT signature verification.
69///
70/// Supports both symmetric (HS256) and asymmetric (RS256/RS384/RS512) algorithms.
71#[derive(Debug, Clone)]
72pub enum SigningKey {
73    /// HMAC-SHA256 symmetric key.
74    ///
75    /// Use for internal services where the same secret is shared
76    /// between token issuer and validator.
77    Hs256(Zeroizing<Vec<u8>>),
78
79    /// HMAC-SHA384 symmetric key.
80    Hs384(Zeroizing<Vec<u8>>),
81
82    /// HMAC-SHA512 symmetric key.
83    Hs512(Zeroizing<Vec<u8>>),
84
85    /// RSA public key in PEM format (RS256 algorithm).
86    ///
87    /// Use for external identity providers. The public key is used
88    /// to verify tokens signed with the provider's private key.
89    Rs256Pem(String),
90
91    /// RSA public key in PEM format (RS384 algorithm).
92    Rs384Pem(String),
93
94    /// RSA public key in PEM format (RS512 algorithm).
95    Rs512Pem(String),
96
97    /// RSA public key components (n, e) for RS256.
98    ///
99    /// Use when receiving keys from JWKS endpoints.
100    Rs256Components {
101        /// RSA modulus (n) in base64url encoding
102        n: String,
103        /// RSA exponent (e) in base64url encoding
104        e: String,
105    },
106}
107
108impl SigningKey {
109    /// Create an HS256 signing key from a secret string.
110    #[must_use]
111    pub fn hs256(secret: &str) -> Self {
112        Self::Hs256(Zeroizing::new(secret.as_bytes().to_vec()))
113    }
114
115    /// Create an HS256 signing key from raw bytes.
116    #[must_use]
117    pub fn hs256_bytes(secret: &[u8]) -> Self {
118        Self::Hs256(Zeroizing::new(secret.to_vec()))
119    }
120
121    /// Create an RS256 signing key from PEM-encoded public key.
122    #[must_use]
123    pub fn rs256_pem(pem: &str) -> Self {
124        Self::Rs256Pem(pem.to_string())
125    }
126
127    /// Create an RS256 signing key from RSA components.
128    ///
129    /// This is useful when parsing JWKS responses.
130    #[must_use]
131    pub fn rs256_components(n: &str, e: &str) -> Self {
132        Self::Rs256Components {
133            n: n.to_string(),
134            e: e.to_string(),
135        }
136    }
137
138    /// Get the algorithm for this signing key.
139    #[must_use]
140    pub const fn algorithm(&self) -> Algorithm {
141        match self {
142            Self::Hs256(_) => Algorithm::HS256,
143            Self::Hs384(_) => Algorithm::HS384,
144            Self::Hs512(_) => Algorithm::HS512,
145            Self::Rs256Pem(_) | Self::Rs256Components { .. } => Algorithm::RS256,
146            Self::Rs384Pem(_) => Algorithm::RS384,
147            Self::Rs512Pem(_) => Algorithm::RS512,
148        }
149    }
150
151    /// Convert to a jsonwebtoken DecodingKey.
152    fn to_decoding_key(&self) -> std::result::Result<DecodingKey, SecurityError> {
153        match self {
154            Self::Hs256(secret) | Self::Hs384(secret) | Self::Hs512(secret) => {
155                Ok(DecodingKey::from_secret(secret))
156            },
157            Self::Rs256Pem(pem) | Self::Rs384Pem(pem) | Self::Rs512Pem(pem) => {
158                DecodingKey::from_rsa_pem(pem.as_bytes()).map_err(|e| {
159                    SecurityError::SecurityConfigError(format!("Invalid RSA PEM key: {e}"))
160                })
161            },
162            Self::Rs256Components { n, e } => DecodingKey::from_rsa_components(n, e).map_err(|e| {
163                SecurityError::SecurityConfigError(format!("Invalid RSA components: {e}"))
164            }),
165        }
166    }
167}
168
169// ============================================================================
170// Authentication Configuration
171// ============================================================================
172
173/// Authentication configuration
174///
175/// Defines what authentication requirements must be met for a request.
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct AuthConfig {
178    /// If true, authentication is required for all requests
179    pub required: bool,
180
181    /// Token lifetime in seconds (for validation purposes)
182    pub token_expiry_secs: u64,
183
184    /// Signing key for JWT signature verification.
185    ///
186    /// If `None`, signature verification is disabled (NOT RECOMMENDED for production).
187    /// Use `SigningKey::hs256()` or `SigningKey::rs256_pem()` to enable verification.
188    #[serde(skip)]
189    pub signing_key: Option<SigningKey>,
190
191    /// Expected issuer (iss claim).
192    ///
193    /// If set, tokens must have this value in their `iss` claim.
194    #[serde(default)]
195    pub issuer: Option<String>,
196
197    /// Expected audience (aud claim).
198    ///
199    /// If set, tokens must have this value in their `aud` claim.
200    #[serde(default)]
201    pub audience: Option<String>,
202
203    /// Clock skew tolerance in seconds.
204    ///
205    /// Allow this many seconds of clock difference when validating exp/nbf claims.
206    /// Default: 60 seconds
207    #[serde(default = "default_clock_skew")]
208    pub clock_skew_secs: u64,
209}
210
211fn default_clock_skew() -> u64 {
212    60
213}
214
215impl AuthConfig {
216    /// Create a permissive authentication configuration (auth optional)
217    ///
218    /// - Authentication optional
219    /// - Token expiry: 3600 seconds (1 hour)
220    /// - No signature verification (for testing only)
221    #[must_use]
222    pub fn permissive() -> Self {
223        Self {
224            required:          false,
225            token_expiry_secs: 3600,
226            signing_key:       None,
227            issuer:            None,
228            audience:          None,
229            clock_skew_secs:   default_clock_skew(),
230        }
231    }
232
233    /// Create a standard authentication configuration (auth required)
234    ///
235    /// - Authentication required
236    /// - Token expiry: 3600 seconds (1 hour)
237    /// - No signature verification (configure `signing_key` for production)
238    #[must_use]
239    pub fn standard() -> Self {
240        Self {
241            required:          true,
242            token_expiry_secs: 3600,
243            signing_key:       None,
244            issuer:            None,
245            audience:          None,
246            clock_skew_secs:   default_clock_skew(),
247        }
248    }
249
250    /// Create a strict authentication configuration (auth required, short expiry)
251    ///
252    /// - Authentication required
253    /// - Token expiry: 1800 seconds (30 minutes)
254    /// - No signature verification (configure `signing_key` for production)
255    #[must_use]
256    pub fn strict() -> Self {
257        Self {
258            required:          true,
259            token_expiry_secs: 1800,
260            signing_key:       None,
261            issuer:            None,
262            audience:          None,
263            clock_skew_secs:   default_clock_skew(),
264        }
265    }
266
267    /// Create a configuration with HS256 signing key.
268    ///
269    /// This is the recommended configuration for production when using
270    /// symmetric key signing (internal services).
271    #[must_use]
272    pub fn with_hs256(secret: &str) -> Self {
273        Self {
274            required:          true,
275            token_expiry_secs: 3600,
276            signing_key:       Some(SigningKey::hs256(secret)),
277            issuer:            None,
278            audience:          None,
279            clock_skew_secs:   default_clock_skew(),
280        }
281    }
282
283    /// Create a configuration with RS256 signing key from PEM.
284    ///
285    /// This is the recommended configuration for production when using
286    /// asymmetric key signing (external identity providers).
287    #[must_use]
288    pub fn with_rs256_pem(pem: &str) -> Self {
289        Self {
290            required:          true,
291            token_expiry_secs: 3600,
292            signing_key:       Some(SigningKey::rs256_pem(pem)),
293            issuer:            None,
294            audience:          None,
295            clock_skew_secs:   default_clock_skew(),
296        }
297    }
298
299    /// Set the expected issuer.
300    #[must_use]
301    pub fn with_issuer(mut self, issuer: &str) -> Self {
302        self.issuer = Some(issuer.to_string());
303        self
304    }
305
306    /// Set the expected audience.
307    #[must_use]
308    pub fn with_audience(mut self, audience: &str) -> Self {
309        self.audience = Some(audience.to_string());
310        self
311    }
312
313    /// Check if signature verification is enabled.
314    #[must_use]
315    pub const fn has_signing_key(&self) -> bool {
316        self.signing_key.is_some()
317    }
318}
319
320/// Authenticated user information extracted from JWT claims
321///
322/// Contains the essential user information needed for authorization checks
323/// and audit logging.
324#[derive(Debug, Clone, PartialEq, Eq)]
325pub struct AuthenticatedUser {
326    /// User ID (from 'sub' claim in JWT)
327    pub user_id: String,
328
329    /// Scopes/permissions (from 'scope' claim if present)
330    pub scopes: Vec<String>,
331
332    /// When the token expires
333    pub expires_at: DateTime<Utc>,
334}
335
336impl fmt::Display for AuthenticatedUser {
337    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338        write!(
339            f,
340            "User({}, expires={})",
341            self.user_id,
342            self.expires_at.format("%Y-%m-%d %H:%M:%S UTC")
343        )
344    }
345}
346
347impl AuthenticatedUser {
348    /// Check if the user has a specific scope
349    #[must_use]
350    pub fn has_scope(&self, scope: &str) -> bool {
351        self.scopes.iter().any(|s| s == scope)
352    }
353
354    /// Check if the token has expired (as of now)
355    #[must_use]
356    pub fn is_expired(&self) -> bool {
357        self.expires_at <= Utc::now()
358    }
359
360    /// Get time until expiry in seconds
361    #[must_use]
362    pub fn ttl_secs(&self) -> i64 {
363        (self.expires_at - Utc::now()).num_seconds()
364    }
365}
366
367/// Request context for authentication validation
368///
369/// Contains information extracted from an HTTP request.
370#[derive(Debug, Clone)]
371pub struct AuthRequest {
372    /// Raw Authorization header value (e.g., "Bearer eyJhbG...")
373    pub authorization_header: Option<String>,
374}
375
376impl AuthRequest {
377    /// Create a new auth request from an authorization header
378    #[must_use]
379    pub fn new(authorization_header: Option<String>) -> Self {
380        Self {
381            authorization_header,
382        }
383    }
384
385    /// Extract the bearer token from the Authorization header
386    ///
387    /// Expected format: `"Bearer <token>"`
388    ///
389    /// Returns Ok(token) if valid format, Err otherwise
390    pub fn extract_bearer_token(&self) -> Result<String> {
391        let header = self.authorization_header.as_ref().ok_or(SecurityError::AuthRequired)?;
392
393        if !header.starts_with("Bearer ") {
394            return Err(SecurityError::AuthRequired);
395        }
396
397        Ok(header[7..].to_string())
398    }
399}
400
401/// Claims extracted from JWT token
402///
403/// This is a simplified representation of JWT claims.
404/// In production, this would be more comprehensive.
405#[derive(Debug, Clone)]
406pub struct TokenClaims {
407    /// Subject (user ID)
408    pub sub: Option<String>,
409
410    /// Expiration timestamp (seconds since epoch)
411    pub exp: Option<i64>,
412
413    /// Scope claim (space-separated string)
414    pub scope: Option<String>,
415
416    /// Audience claim
417    pub aud: Option<Vec<String>>,
418
419    /// Issuer claim
420    pub iss: Option<String>,
421}
422
423/// JWT claims structure for jsonwebtoken crate deserialization.
424///
425/// This struct is used internally for decoding and validating JWT tokens
426/// when signature verification is enabled.
427#[derive(Debug, Deserialize)]
428struct JwtClaims {
429    /// Subject (user ID) - required
430    sub: Option<String>,
431
432    /// Expiration timestamp (seconds since epoch) - required
433    exp: Option<i64>,
434
435    /// Issued at timestamp (captured but not used directly)
436    #[serde(default)]
437    #[allow(dead_code)] // Reason: serde deserialization target, validated by jsonwebtoken
438    iat: Option<i64>,
439
440    /// Not before timestamp (captured but not used directly)
441    #[serde(default)]
442    #[allow(dead_code)] // Reason: serde deserialization target, validated by jsonwebtoken
443    nbf: Option<i64>,
444
445    /// Scope claim (space-separated string)
446    #[serde(default)]
447    scope: Option<String>,
448
449    /// Scopes as array (alternative format used by some providers)
450    #[serde(default)]
451    scp: Option<Vec<String>>,
452
453    /// Permissions (Auth0 RBAC style)
454    #[serde(default)]
455    permissions: Option<Vec<String>>,
456
457    /// Audience claim (validated by jsonwebtoken, captured for logging)
458    #[serde(default)]
459    #[allow(dead_code)] // Reason: serde deserialization target, validated by jsonwebtoken
460    aud: Option<serde_json::Value>,
461
462    /// Issuer claim (validated by jsonwebtoken, captured for logging)
463    #[serde(default)]
464    #[allow(dead_code)] // Reason: serde deserialization target, validated by jsonwebtoken
465    iss: Option<String>,
466}
467
468/// Authentication Middleware
469///
470/// Validates incoming requests for authentication requirements.
471/// Acts as the second layer in the security middleware pipeline.
472#[derive(Debug, Clone)]
473pub struct AuthMiddleware {
474    config: AuthConfig,
475}
476
477impl AuthMiddleware {
478    /// Create a new authentication middleware from configuration
479    #[must_use]
480    pub fn from_config(config: AuthConfig) -> Self {
481        Self { config }
482    }
483
484    /// Create middleware with permissive settings (authentication optional)
485    #[must_use]
486    pub fn permissive() -> Self {
487        Self::from_config(AuthConfig::permissive())
488    }
489
490    /// Create middleware with standard settings (authentication required)
491    #[must_use]
492    pub fn standard() -> Self {
493        Self::from_config(AuthConfig::standard())
494    }
495
496    /// Create middleware with strict settings (authentication required, short expiry)
497    #[must_use]
498    pub fn strict() -> Self {
499        Self::from_config(AuthConfig::strict())
500    }
501
502    /// Validate authentication in a request
503    ///
504    /// Performs validation checks in order:
505    /// 1. Extract token from Authorization header
506    /// 2. Validate token signature (if signing key configured)
507    /// 3. Check token expiry (exp claim)
508    /// 4. Validate issuer/audience claims (if configured)
509    /// 5. Extract required claims (sub)
510    /// 6. Extract optional claims (scope, aud, iss)
511    ///
512    /// Returns AuthenticatedUser if valid, Err if any check fails.
513    pub fn validate_request(&self, req: &AuthRequest) -> Result<AuthenticatedUser> {
514        // Check 1: Extract token from Authorization header
515        let token = self.extract_token(req)?;
516
517        // Check 2: Validate token (with or without signature verification)
518        if let Some(ref signing_key) = self.config.signing_key {
519            // Use jsonwebtoken crate for proper signature verification
520            self.validate_token_with_signature(&token, signing_key)
521        } else {
522            // Fallback: structure validation only (for testing/backwards compatibility)
523            // WARNING: This is insecure for production use!
524            self.validate_token_structure_only(&token)
525        }
526    }
527
528    /// Validate token with cryptographic signature verification.
529    ///
530    /// This is the secure path used when a signing key is configured.
531    fn validate_token_with_signature(
532        &self,
533        token: &str,
534        signing_key: &SigningKey,
535    ) -> Result<AuthenticatedUser> {
536        // Get the decoding key
537        let decoding_key = signing_key.to_decoding_key()?;
538
539        // Build validation configuration
540        let mut validation = Validation::new(signing_key.algorithm());
541
542        // Configure issuer validation (only validate if configured)
543        if let Some(ref issuer) = self.config.issuer {
544            validation.set_issuer(&[issuer]);
545        }
546        // Note: If issuer is not set, validation.iss is None and won't be validated
547
548        // Configure audience validation
549        if let Some(ref audience) = self.config.audience {
550            validation.set_audience(&[audience]);
551        } else {
552            validation.validate_aud = false;
553        }
554
555        // Set clock skew tolerance
556        validation.leeway = self.config.clock_skew_secs;
557
558        // Decode and validate the token
559        let token_data = decode::<JwtClaims>(token, &decoding_key, &validation).map_err(|e| {
560            match e.kind() {
561                jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
562                    // Try to extract the actual expiry time from the token
563                    SecurityError::TokenExpired {
564                        expired_at: Utc::now(), // Approximate - actual time is not accessible
565                    }
566                },
567                jsonwebtoken::errors::ErrorKind::InvalidSignature => SecurityError::InvalidToken,
568                jsonwebtoken::errors::ErrorKind::InvalidIssuer => SecurityError::InvalidToken,
569                jsonwebtoken::errors::ErrorKind::InvalidAudience => SecurityError::InvalidToken,
570                jsonwebtoken::errors::ErrorKind::InvalidAlgorithm => {
571                    SecurityError::InvalidTokenAlgorithm {
572                        algorithm: format!("{:?}", signing_key.algorithm()),
573                    }
574                },
575                jsonwebtoken::errors::ErrorKind::MissingRequiredClaim(claim) => {
576                    SecurityError::TokenMissingClaim {
577                        claim: claim.clone(),
578                    }
579                },
580                _ => SecurityError::InvalidToken,
581            }
582        })?;
583
584        let claims = token_data.claims;
585
586        // Extract scopes (supports multiple formats)
587        let scopes = self.extract_scopes_from_jwt_claims(&claims);
588
589        // Extract user ID (required)
590        let user_id = claims.sub.ok_or(SecurityError::TokenMissingClaim {
591            claim: "sub".to_string(),
592        })?;
593
594        // Extract expiration (required)
595        let exp = claims.exp.ok_or(SecurityError::TokenMissingClaim {
596            claim: "exp".to_string(),
597        })?;
598
599        let expires_at =
600            DateTime::<Utc>::from_timestamp(exp, 0).ok_or(SecurityError::InvalidToken)?;
601
602        Ok(AuthenticatedUser {
603            user_id,
604            scopes,
605            expires_at,
606        })
607    }
608
609    /// Extract scopes from JWT claims.
610    ///
611    /// Supports multiple formats:
612    /// - `scope`: space-separated string (OAuth2 standard)
613    /// - `scp`: array of strings (Microsoft)
614    /// - `permissions`: array of strings (Auth0 RBAC)
615    fn extract_scopes_from_jwt_claims(&self, claims: &JwtClaims) -> Vec<String> {
616        // Try space-separated scope string first (most common)
617        if let Some(ref scope) = claims.scope {
618            return scope.split_whitespace().map(String::from).collect();
619        }
620
621        // Try array of scopes (scp claim)
622        if let Some(ref scp) = claims.scp {
623            return scp.clone();
624        }
625
626        // Try permissions array (Auth0 RBAC)
627        if let Some(ref permissions) = claims.permissions {
628            return permissions.clone();
629        }
630
631        Vec::new()
632    }
633
634    /// Validate token structure only (no signature verification).
635    ///
636    /// WARNING: This is insecure and should only be used for testing
637    /// or when signature verification is handled elsewhere.
638    fn validate_token_structure_only(&self, token: &str) -> Result<AuthenticatedUser> {
639        // Validate basic structure
640        self.validate_token_structure(token)?;
641
642        // Parse claims
643        let claims = self.parse_claims(token)?;
644
645        // Extract and validate 'exp' claim (required)
646        let exp = claims.exp.ok_or(SecurityError::TokenMissingClaim {
647            claim: "exp".to_string(),
648        })?;
649
650        // Check expiry
651        let expires_at =
652            DateTime::<Utc>::from_timestamp(exp, 0).ok_or(SecurityError::InvalidToken)?;
653
654        if expires_at <= Utc::now() {
655            return Err(SecurityError::TokenExpired {
656                expired_at: expires_at,
657            });
658        }
659
660        // Extract and validate 'sub' claim (required)
661        let user_id = claims.sub.ok_or(SecurityError::TokenMissingClaim {
662            claim: "sub".to_string(),
663        })?;
664
665        // Extract optional claims
666        let scopes = claims
667            .scope
668            .as_ref()
669            .map(|s| s.split_whitespace().map(String::from).collect())
670            .unwrap_or_default();
671
672        Ok(AuthenticatedUser {
673            user_id,
674            scopes,
675            expires_at,
676        })
677    }
678
679    /// Extract token from the authorization header
680    fn extract_token(&self, req: &AuthRequest) -> Result<String> {
681        // If auth is not required and no header present, that's OK
682        if !self.config.required && req.authorization_header.is_none() {
683            return Err(SecurityError::AuthRequired); // Will be handled differently
684        }
685
686        req.extract_bearer_token()
687    }
688
689    /// Validate token structure (basic checks)
690    ///
691    /// A real implementation would validate the signature here.
692    /// For now, we just check basic structure.
693    fn validate_token_structure(&self, token: &str) -> Result<()> {
694        // JWT has 3 parts separated by dots: header.payload.signature
695        let parts: Vec<&str> = token.split('.').collect();
696        if parts.len() != 3 {
697            return Err(SecurityError::InvalidToken);
698        }
699
700        // Check that each part is non-empty
701        if parts.iter().any(|p| p.is_empty()) {
702            return Err(SecurityError::InvalidToken);
703        }
704
705        Ok(())
706    }
707
708    /// Parse JWT claims (simplified, for demo purposes)
709    ///
710    /// In a real implementation, this would decode and validate the JWT signature.
711    /// For testing, we accept a special test token format: "test:{json_payload}"
712    fn parse_claims(&self, token: &str) -> Result<TokenClaims> {
713        // Split the token
714        let parts: Vec<&str> = token.split('.').collect();
715        if parts.len() != 3 {
716            return Err(SecurityError::InvalidToken);
717        }
718
719        // For testing, we use a simple format: part1.{json}.part3
720        // where {json} is a base64-like encoded JSON
721        // Since we don't have base64 in core dependencies, we'll try to parse directly
722        let payload_part = parts[1];
723
724        // Try to decode as hex (simpler than base64 and no dependencies)
725        // For test tokens, we'll encode the JSON as hex
726        if let Ok(decoded) = hex::decode(payload_part) {
727            if let Ok(json_str) = std::str::from_utf8(&decoded) {
728                if let Ok(json) = serde_json::from_str::<serde_json::Value>(json_str) {
729                    return Ok(self.extract_claims_from_json(&json));
730                }
731            }
732        }
733
734        // If hex decoding fails, try to parse as UTF-8 directly (for test tokens created inline)
735        if let Ok(json) = serde_json::from_str::<serde_json::Value>(payload_part) {
736            return Ok(self.extract_claims_from_json(&json));
737        }
738
739        Err(SecurityError::InvalidToken)
740    }
741
742    /// Extract claims from parsed JSON
743    fn extract_claims_from_json(&self, json: &serde_json::Value) -> TokenClaims {
744        let sub = json["sub"].as_str().map(String::from);
745        let exp = json["exp"].as_i64();
746        let scope = json["scope"].as_str().map(String::from);
747        let aud = json["aud"]
748            .as_array()
749            .map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect());
750        let iss = json["iss"].as_str().map(String::from);
751
752        TokenClaims {
753            sub,
754            exp,
755            scope,
756            aud,
757            iss,
758        }
759    }
760
761    /// Get the underlying configuration
762    #[must_use]
763    pub const fn config(&self) -> &AuthConfig {
764        &self.config
765    }
766}
767
768#[cfg(test)]
769mod tests {
770    use super::*;
771
772    // ============================================================================
773    // Helper Functions
774    // ============================================================================
775
776    /// Create a valid JWT token with specified claims (for testing)
777    ///
778    /// Note: This creates a structurally valid JWT, but doesn't sign it.
779    /// For real use, you'd use a proper JWT library.
780    fn create_test_token(sub: &str, exp_offset_secs: i64, scope: Option<&str>) -> String {
781        let now = chrono::Utc::now().timestamp();
782        let exp = now + exp_offset_secs;
783
784        // Create payload
785        let mut payload = serde_json::json!({
786            "sub": sub,
787            "exp": exp,
788            "iat": now,
789            "aud": ["test-audience"],
790            "iss": "test-issuer"
791        });
792
793        if let Some(s) = scope {
794            payload["scope"] = serde_json::json!(s);
795        }
796
797        // Encode payload as hex for testing
798        let payload_json = payload.to_string();
799        let payload_hex = hex::encode(&payload_json);
800        let signature = "test_signature"; // Not a real signature
801
802        // Format: header.payload_hex.signature
803        format!("header.{payload_hex}.{signature}")
804    }
805
806    // ============================================================================
807    // Check 1: Token Extraction Tests
808    // ============================================================================
809
810    #[test]
811    fn test_bearer_token_extracted_correctly() {
812        let token = "test_token_12345";
813        let req = AuthRequest::new(Some(format!("Bearer {token}")));
814
815        let result = req.extract_bearer_token();
816        assert!(result.is_ok());
817        assert_eq!(result.unwrap(), token);
818    }
819
820    #[test]
821    fn test_missing_authorization_header_rejected() {
822        let req = AuthRequest::new(None);
823
824        let result = req.extract_bearer_token();
825        assert!(matches!(result, Err(SecurityError::AuthRequired)));
826    }
827
828    #[test]
829    fn test_invalid_authorization_header_format_rejected() {
830        let req = AuthRequest::new(Some("Basic abc123".to_string()));
831
832        let result = req.extract_bearer_token();
833        assert!(matches!(result, Err(SecurityError::AuthRequired)));
834    }
835
836    #[test]
837    fn test_bearer_prefix_required() {
838        let req = AuthRequest::new(Some("abc123".to_string()));
839
840        let result = req.extract_bearer_token();
841        assert!(matches!(result, Err(SecurityError::AuthRequired)));
842    }
843
844    // ============================================================================
845    // Check 2: Token Structure Validation Tests
846    // ============================================================================
847
848    #[test]
849    fn test_valid_token_structure() {
850        let middleware = AuthMiddleware::permissive();
851        let token = create_test_token("user123", 3600, None);
852
853        let result = middleware.validate_token_structure(&token);
854        assert!(result.is_ok());
855    }
856
857    #[test]
858    fn test_token_with_wrong_part_count_rejected() {
859        let middleware = AuthMiddleware::permissive();
860        let token = "header.payload"; // Missing signature
861
862        let result = middleware.validate_token_structure(token);
863        assert!(matches!(result, Err(SecurityError::InvalidToken)));
864    }
865
866    #[test]
867    fn test_token_with_empty_part_rejected() {
868        let middleware = AuthMiddleware::permissive();
869        let token = "header..signature"; // Empty payload
870
871        let result = middleware.validate_token_structure(token);
872        assert!(matches!(result, Err(SecurityError::InvalidToken)));
873    }
874
875    // ============================================================================
876    // Check 3: Token Expiry Validation Tests
877    // ============================================================================
878
879    #[test]
880    fn test_valid_token_not_expired() {
881        let middleware = AuthMiddleware::standard();
882        let token = create_test_token("user123", 3600, None); // 1 hour from now
883        let req = AuthRequest::new(Some(format!("Bearer {token}")));
884
885        let result = middleware.validate_request(&req);
886        assert!(result.is_ok());
887    }
888
889    #[test]
890    fn test_expired_token_rejected() {
891        let middleware = AuthMiddleware::standard();
892        let token = create_test_token("user123", -3600, None); // 1 hour ago
893        let req = AuthRequest::new(Some(format!("Bearer {token}")));
894
895        let result = middleware.validate_request(&req);
896        assert!(matches!(result, Err(SecurityError::TokenExpired { .. })));
897    }
898
899    #[test]
900    fn test_token_expiring_now_rejected() {
901        let middleware = AuthMiddleware::standard();
902        // Token that expires at the current timestamp (or in past due to processing time)
903        let token = create_test_token("user123", 0, None);
904        let req = AuthRequest::new(Some(format!("Bearer {token}")));
905
906        // May pass or fail depending on exact timing, but should be close
907        let result = middleware.validate_request(&req);
908        // We won't assert here since timing is critical
909        let _ = result;
910    }
911
912    // ============================================================================
913    // Check 4: Required Claims Validation Tests
914    // ============================================================================
915
916    #[test]
917    fn test_missing_sub_claim_rejected() {
918        let middleware = AuthMiddleware::standard();
919
920        // Create token without 'sub' claim
921        let now = chrono::Utc::now().timestamp();
922        let payload = serde_json::json!({
923            "exp": now + 3600,
924            "iat": now
925        });
926
927        let payload_hex = hex::encode(payload.to_string());
928        let token = format!("header.{payload_hex}.signature");
929
930        let req = AuthRequest::new(Some(format!("Bearer {token}")));
931        let result = middleware.validate_request(&req);
932
933        assert!(matches!(
934            result,
935            Err(SecurityError::TokenMissingClaim { claim })
936            if claim == "sub"
937        ));
938    }
939
940    #[test]
941    fn test_missing_exp_claim_rejected() {
942        let middleware = AuthMiddleware::standard();
943
944        // Create token without 'exp' claim
945        let payload = serde_json::json!({
946            "sub": "user123",
947            "iat": chrono::Utc::now().timestamp()
948        });
949
950        let payload_hex = hex::encode(payload.to_string());
951        let token = format!("header.{payload_hex}.signature");
952
953        let req = AuthRequest::new(Some(format!("Bearer {token}")));
954        let result = middleware.validate_request(&req);
955
956        assert!(matches!(
957            result,
958            Err(SecurityError::TokenMissingClaim { claim })
959            if claim == "exp"
960        ));
961    }
962
963    // ============================================================================
964    // Check 5: User Info Extraction Tests
965    // ============================================================================
966
967    #[test]
968    fn test_user_id_extracted_from_token() {
969        let middleware = AuthMiddleware::standard();
970        let token = create_test_token("user_12345", 3600, None);
971        let req = AuthRequest::new(Some(format!("Bearer {token}")));
972
973        let result = middleware.validate_request(&req);
974        assert!(result.is_ok());
975
976        let user = result.unwrap();
977        assert_eq!(user.user_id, "user_12345");
978    }
979
980    #[test]
981    fn test_scopes_extracted_from_token() {
982        let middleware = AuthMiddleware::standard();
983        let token = create_test_token("user123", 3600, Some("read write admin"));
984        let req = AuthRequest::new(Some(format!("Bearer {token}")));
985
986        let result = middleware.validate_request(&req);
987        assert!(result.is_ok());
988
989        let user = result.unwrap();
990        assert_eq!(user.scopes, vec!["read", "write", "admin"]);
991    }
992
993    #[test]
994    fn test_empty_scopes_when_scope_claim_absent() {
995        let middleware = AuthMiddleware::standard();
996        let token = create_test_token("user123", 3600, None);
997        let req = AuthRequest::new(Some(format!("Bearer {token}")));
998
999        let result = middleware.validate_request(&req);
1000        assert!(result.is_ok());
1001
1002        let user = result.unwrap();
1003        assert!(user.scopes.is_empty());
1004    }
1005
1006    #[test]
1007    fn test_expires_at_extracted_correctly() {
1008        let middleware = AuthMiddleware::standard();
1009        let offset_secs = 7200; // 2 hours
1010
1011        let token = create_test_token("user123", offset_secs, None);
1012        let req = AuthRequest::new(Some(format!("Bearer {token}")));
1013
1014        let result = middleware.validate_request(&req);
1015        assert!(result.is_ok());
1016
1017        let user = result.unwrap();
1018        let now = Utc::now();
1019        let diff = (user.expires_at - now).num_seconds();
1020
1021        // Should be approximately offset_secs (within 5 seconds due to processing)
1022        assert!((offset_secs - 5..=offset_secs + 5).contains(&diff));
1023    }
1024
1025    // ============================================================================
1026    // Configuration Tests
1027    // ============================================================================
1028
1029    #[test]
1030    fn test_permissive_config() {
1031        let config = AuthConfig::permissive();
1032        assert!(!config.required);
1033        assert_eq!(config.token_expiry_secs, 3600);
1034    }
1035
1036    #[test]
1037    fn test_standard_config() {
1038        let config = AuthConfig::standard();
1039        assert!(config.required);
1040        assert_eq!(config.token_expiry_secs, 3600);
1041    }
1042
1043    #[test]
1044    fn test_strict_config() {
1045        let config = AuthConfig::strict();
1046        assert!(config.required);
1047        assert_eq!(config.token_expiry_secs, 1800);
1048    }
1049
1050    #[test]
1051    fn test_middleware_helpers() {
1052        let permissive = AuthMiddleware::permissive();
1053        assert!(!permissive.config().required);
1054
1055        let standard = AuthMiddleware::standard();
1056        assert!(standard.config().required);
1057
1058        let strict = AuthMiddleware::strict();
1059        assert!(strict.config().required);
1060    }
1061
1062    // ============================================================================
1063    // AuthenticatedUser Tests
1064    // ============================================================================
1065
1066    #[test]
1067    fn test_user_has_scope() {
1068        let user = AuthenticatedUser {
1069            user_id:    "user123".to_string(),
1070            scopes:     vec!["read".to_string(), "write".to_string()],
1071            expires_at: Utc::now() + chrono::Duration::hours(1),
1072        };
1073
1074        assert!(user.has_scope("read"));
1075        assert!(user.has_scope("write"));
1076        assert!(!user.has_scope("admin"));
1077    }
1078
1079    #[test]
1080    fn test_user_is_not_expired() {
1081        let user = AuthenticatedUser {
1082            user_id:    "user123".to_string(),
1083            scopes:     vec![],
1084            expires_at: Utc::now() + chrono::Duration::hours(1),
1085        };
1086
1087        assert!(!user.is_expired());
1088    }
1089
1090    #[test]
1091    fn test_user_is_expired() {
1092        let user = AuthenticatedUser {
1093            user_id:    "user123".to_string(),
1094            scopes:     vec![],
1095            expires_at: Utc::now() - chrono::Duration::hours(1),
1096        };
1097
1098        assert!(user.is_expired());
1099    }
1100
1101    #[test]
1102    fn test_user_ttl_calculation() {
1103        let now = Utc::now();
1104        let expires_at = now + chrono::Duration::hours(2);
1105        let user = AuthenticatedUser {
1106            user_id: "user123".to_string(),
1107            scopes: vec![],
1108            expires_at,
1109        };
1110
1111        let ttl = user.ttl_secs();
1112        // Should be approximately 7200 seconds (2 hours)
1113        assert!((7195..=7205).contains(&ttl));
1114    }
1115
1116    #[test]
1117    fn test_user_display() {
1118        let user = AuthenticatedUser {
1119            user_id:    "user123".to_string(),
1120            scopes:     vec![],
1121            expires_at: Utc::now() + chrono::Duration::hours(1),
1122        };
1123
1124        let display_str = user.to_string();
1125        assert!(display_str.contains("user123"));
1126        assert!(display_str.contains("UTC"));
1127    }
1128
1129    // ============================================================================
1130    // Error Message Tests
1131    // ============================================================================
1132
1133    #[test]
1134    fn test_error_messages_clear_and_actionable() {
1135        let middleware = AuthMiddleware::standard();
1136
1137        // Test missing header error
1138        let req = AuthRequest::new(None);
1139        let result = middleware.validate_request(&req);
1140        assert!(matches!(result, Err(SecurityError::AuthRequired)));
1141
1142        // Test invalid format error
1143        let req = AuthRequest::new(Some("Basic xyz".to_string()));
1144        let result = middleware.validate_request(&req);
1145        assert!(matches!(result, Err(SecurityError::AuthRequired)));
1146    }
1147
1148    // ============================================================================
1149    // Edge Cases
1150    // ============================================================================
1151
1152    #[test]
1153    fn test_auth_not_required_allows_missing_token() {
1154        // When auth is NOT required, missing token should still go through extraction
1155        let middleware = AuthMiddleware::permissive(); // required = false
1156        let req = AuthRequest::new(None);
1157
1158        let result = middleware.validate_request(&req);
1159        // Should fail at extraction, not because auth is optional
1160        assert!(matches!(result, Err(SecurityError::AuthRequired)));
1161    }
1162
1163    #[test]
1164    fn test_whitespace_in_scopes_handled() {
1165        let middleware = AuthMiddleware::standard();
1166        let token = create_test_token("user123", 3600, Some("  read   write  admin  "));
1167        let req = AuthRequest::new(Some(format!("Bearer {token}")));
1168
1169        let result = middleware.validate_request(&req);
1170        assert!(result.is_ok());
1171
1172        let user = result.unwrap();
1173        // split_whitespace handles multiple spaces correctly
1174        assert_eq!(user.scopes.len(), 3);
1175    }
1176
1177    #[test]
1178    fn test_single_scope_parsed_correctly() {
1179        let middleware = AuthMiddleware::standard();
1180        let token = create_test_token("user123", 3600, Some("read"));
1181        let req = AuthRequest::new(Some(format!("Bearer {token}")));
1182
1183        let result = middleware.validate_request(&req);
1184        assert!(result.is_ok());
1185
1186        let user = result.unwrap();
1187        assert_eq!(user.scopes, vec!["read"]);
1188    }
1189
1190    // ============================================================================
1191    // JWT Signature Verification Tests (Issue #225)
1192    // ============================================================================
1193
1194    /// Helper to create a properly signed HS256 JWT token
1195    #[allow(clippy::items_after_statements)] // Reason: test helper structs defined near point of use for readability
1196    fn create_signed_hs256_token(
1197        sub: &str,
1198        exp_offset_secs: i64,
1199        scope: Option<&str>,
1200        secret: &str,
1201    ) -> String {
1202        use jsonwebtoken::{EncodingKey, Header, encode};
1203
1204        let now = chrono::Utc::now().timestamp();
1205        let exp = now + exp_offset_secs;
1206
1207        #[derive(serde::Serialize)]
1208        struct Claims {
1209            sub:   String,
1210            exp:   i64,
1211            iat:   i64,
1212            #[serde(skip_serializing_if = "Option::is_none")]
1213            scope: Option<String>,
1214        }
1215
1216        let claims = Claims {
1217            sub: sub.to_string(),
1218            exp,
1219            iat: now,
1220            scope: scope.map(String::from),
1221        };
1222
1223        encode(
1224            &Header::default(), // HS256
1225            &claims,
1226            &EncodingKey::from_secret(secret.as_bytes()),
1227        )
1228        .expect("Failed to create test token")
1229    }
1230
1231    #[test]
1232    fn test_hs256_signature_verification_valid_token() {
1233        let secret = "super-secret-key-for-testing-only";
1234        let config = AuthConfig::with_hs256(secret);
1235        let middleware = AuthMiddleware::from_config(config);
1236
1237        let token = create_signed_hs256_token("user123", 3600, Some("read write"), secret);
1238        let req = AuthRequest::new(Some(format!("Bearer {token}")));
1239
1240        let result = middleware.validate_request(&req);
1241        assert!(result.is_ok(), "Expected valid token, got: {:?}", result);
1242
1243        let user = result.unwrap();
1244        assert_eq!(user.user_id, "user123");
1245        assert_eq!(user.scopes, vec!["read", "write"]);
1246    }
1247
1248    #[test]
1249    fn test_hs256_signature_verification_wrong_secret_rejected() {
1250        let signing_secret = "correct-secret";
1251        let wrong_secret = "wrong-secret";
1252
1253        let config = AuthConfig::with_hs256(signing_secret);
1254        let middleware = AuthMiddleware::from_config(config);
1255
1256        // Token signed with wrong secret
1257        let token = create_signed_hs256_token("user123", 3600, None, wrong_secret);
1258        let req = AuthRequest::new(Some(format!("Bearer {token}")));
1259
1260        let result = middleware.validate_request(&req);
1261        assert!(
1262            matches!(result, Err(SecurityError::InvalidToken)),
1263            "Expected InvalidToken for wrong signature, got: {:?}",
1264            result
1265        );
1266    }
1267
1268    #[test]
1269    fn test_hs256_expired_token_rejected() {
1270        let secret = "test-secret";
1271        let config = AuthConfig::with_hs256(secret);
1272        let middleware = AuthMiddleware::from_config(config);
1273
1274        // Token expired 1 hour ago
1275        let token = create_signed_hs256_token("user123", -3600, None, secret);
1276        let req = AuthRequest::new(Some(format!("Bearer {token}")));
1277
1278        let result = middleware.validate_request(&req);
1279        assert!(
1280            matches!(result, Err(SecurityError::TokenExpired { .. })),
1281            "Expected TokenExpired, got: {:?}",
1282            result
1283        );
1284    }
1285
1286    #[test]
1287    #[allow(clippy::items_after_statements)] // Reason: test helper structs defined near point of use for readability
1288    fn test_hs256_with_issuer_validation() {
1289        use jsonwebtoken::{EncodingKey, Header, encode};
1290
1291        #[derive(serde::Serialize)]
1292        struct ClaimsWithIss {
1293            sub: String,
1294            exp: i64,
1295            iss: String,
1296        }
1297
1298        let secret = "test-secret";
1299        let config = AuthConfig::with_hs256(secret).with_issuer("https://auth.example.com");
1300        let middleware = AuthMiddleware::from_config(config);
1301
1302        // Create token with matching issuer
1303        let now = chrono::Utc::now().timestamp();
1304        let claims = ClaimsWithIss {
1305            sub: "user123".to_string(),
1306            exp: now + 3600,
1307            iss: "https://auth.example.com".to_string(),
1308        };
1309
1310        let token =
1311            encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes()))
1312                .unwrap();
1313
1314        let req = AuthRequest::new(Some(format!("Bearer {token}")));
1315        let result = middleware.validate_request(&req);
1316        assert!(result.is_ok(), "Expected valid token with issuer, got: {:?}", result);
1317    }
1318
1319    #[test]
1320    #[allow(clippy::items_after_statements)] // Reason: test helper structs defined near point of use for readability
1321    fn test_hs256_with_wrong_issuer_rejected() {
1322        use jsonwebtoken::{EncodingKey, Header, encode};
1323
1324        #[derive(serde::Serialize)]
1325        struct ClaimsWithIss {
1326            sub: String,
1327            exp: i64,
1328            iss: String,
1329        }
1330
1331        let secret = "test-secret";
1332        let config = AuthConfig::with_hs256(secret).with_issuer("https://auth.example.com");
1333        let middleware = AuthMiddleware::from_config(config);
1334
1335        // Create token with wrong issuer
1336        let now = chrono::Utc::now().timestamp();
1337        let claims = ClaimsWithIss {
1338            sub: "user123".to_string(),
1339            exp: now + 3600,
1340            iss: "https://wrong-issuer.com".to_string(),
1341        };
1342
1343        let token =
1344            encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes()))
1345                .unwrap();
1346
1347        let req = AuthRequest::new(Some(format!("Bearer {token}")));
1348        let result = middleware.validate_request(&req);
1349        assert!(
1350            matches!(result, Err(SecurityError::InvalidToken)),
1351            "Expected InvalidToken for wrong issuer, got: {:?}",
1352            result
1353        );
1354    }
1355
1356    #[test]
1357    #[allow(clippy::items_after_statements)] // Reason: test helper structs defined near point of use for readability
1358    fn test_hs256_with_audience_validation() {
1359        use jsonwebtoken::{EncodingKey, Header, encode};
1360
1361        #[derive(serde::Serialize)]
1362        struct ClaimsWithAud {
1363            sub: String,
1364            exp: i64,
1365            aud: String,
1366        }
1367
1368        let secret = "test-secret";
1369        let config = AuthConfig::with_hs256(secret).with_audience("my-api");
1370        let middleware = AuthMiddleware::from_config(config);
1371
1372        // Create token with matching audience
1373        let now = chrono::Utc::now().timestamp();
1374        let claims = ClaimsWithAud {
1375            sub: "user123".to_string(),
1376            exp: now + 3600,
1377            aud: "my-api".to_string(),
1378        };
1379
1380        let token =
1381            encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes()))
1382                .unwrap();
1383
1384        let req = AuthRequest::new(Some(format!("Bearer {token}")));
1385        let result = middleware.validate_request(&req);
1386        assert!(result.is_ok(), "Expected valid token with audience, got: {:?}", result);
1387    }
1388
1389    #[test]
1390    fn test_signing_key_algorithm_detection() {
1391        use jsonwebtoken::Algorithm;
1392
1393        let hs256 = SigningKey::hs256("secret");
1394        assert!(matches!(hs256.algorithm(), Algorithm::HS256));
1395
1396        let hs384 = SigningKey::Hs384(Zeroizing::new(b"secret".to_vec()));
1397        assert!(matches!(hs384.algorithm(), Algorithm::HS384));
1398
1399        let hs512 = SigningKey::Hs512(Zeroizing::new(b"secret".to_vec()));
1400        assert!(matches!(hs512.algorithm(), Algorithm::HS512));
1401
1402        let rs256_pem = SigningKey::rs256_pem("fake-pem");
1403        assert!(matches!(rs256_pem.algorithm(), Algorithm::RS256));
1404
1405        let rs256_comp = SigningKey::rs256_components("n", "e");
1406        assert!(matches!(rs256_comp.algorithm(), Algorithm::RS256));
1407    }
1408
1409    #[test]
1410    fn test_config_has_signing_key() {
1411        let config_without = AuthConfig::standard();
1412        assert!(!config_without.has_signing_key());
1413
1414        let config_with = AuthConfig::with_hs256("secret");
1415        assert!(config_with.has_signing_key());
1416    }
1417
1418    #[test]
1419    fn test_config_builder_pattern() {
1420        let config = AuthConfig::with_hs256("secret")
1421            .with_issuer("https://auth.example.com")
1422            .with_audience("my-api");
1423
1424        assert!(config.has_signing_key());
1425        assert_eq!(config.issuer, Some("https://auth.example.com".to_string()));
1426        assert_eq!(config.audience, Some("my-api".to_string()));
1427    }
1428
1429    #[test]
1430    fn test_malformed_token_rejected_with_signature_verification() {
1431        let config = AuthConfig::with_hs256("secret");
1432        let middleware = AuthMiddleware::from_config(config);
1433
1434        // Not a valid JWT at all
1435        let req = AuthRequest::new(Some("Bearer not-a-jwt".to_string()));
1436        let result = middleware.validate_request(&req);
1437        assert!(
1438            matches!(result, Err(SecurityError::InvalidToken)),
1439            "Expected InvalidToken for malformed JWT, got: {:?}",
1440            result
1441        );
1442    }
1443
1444    #[test]
1445    fn test_tampered_payload_rejected() {
1446        let secret = "test-secret";
1447        let config = AuthConfig::with_hs256(secret);
1448        let middleware = AuthMiddleware::from_config(config);
1449
1450        // Create a valid token
1451        let token = create_signed_hs256_token("user123", 3600, None, secret);
1452
1453        // Tamper with the payload (change middle part)
1454        let parts: Vec<&str> = token.split('.').collect();
1455        let tampered_token = format!("{}.dGFtcGVyZWQ.{}", parts[0], parts[2]);
1456
1457        let req = AuthRequest::new(Some(format!("Bearer {tampered_token}")));
1458        let result = middleware.validate_request(&req);
1459        assert!(
1460            matches!(result, Err(SecurityError::InvalidToken)),
1461            "Expected InvalidToken for tampered payload, got: {:?}",
1462            result
1463        );
1464    }
1465
1466    #[test]
1467    fn test_clock_skew_tolerance() {
1468        let secret = "test-secret";
1469        let mut config = AuthConfig::with_hs256(secret);
1470        config.clock_skew_secs = 120; // 2 minutes tolerance
1471        let middleware = AuthMiddleware::from_config(config);
1472
1473        // Token that expired 30 seconds ago (within 2 minute tolerance)
1474        let token = create_signed_hs256_token("user123", -30, None, secret);
1475        let req = AuthRequest::new(Some(format!("Bearer {token}")));
1476
1477        let result = middleware.validate_request(&req);
1478        // Should still be valid due to clock skew tolerance
1479        assert!(result.is_ok(), "Expected valid token within clock skew, got: {:?}", result);
1480    }
1481}