Skip to main content

fraiseql_core/security/auth_middleware/
middleware.rs

1//! Authentication middleware implementation.
2
3use chrono::{DateTime, Utc};
4use jsonwebtoken::{Validation, decode};
5
6use super::{
7    config::AuthConfig,
8    signing_key::SigningKey,
9    types::{AuthRequest, AuthenticatedUser, JwtClaims, TokenClaims},
10};
11use crate::security::errors::{Result, SecurityError};
12
13/// Authentication Middleware
14///
15/// Validates incoming requests for authentication requirements.
16/// Acts as the second layer in the security middleware pipeline.
17#[derive(Debug, Clone)]
18pub struct AuthMiddleware {
19    config: AuthConfig,
20}
21
22impl AuthMiddleware {
23    /// Create a new authentication middleware from configuration.
24    ///
25    /// Emits a warning when `required = true` but no signing key is configured,
26    /// because JWT signature verification will be disabled in that case.
27    #[must_use]
28    pub fn from_config(config: AuthConfig) -> Self {
29        if config.required && config.signing_key.is_none() {
30            tracing::warn!(
31                "AuthMiddleware: required=true but no signing_key configured — \
32                 JWT signatures will NOT be verified"
33            );
34        }
35        Self { config }
36    }
37
38    /// Create middleware with permissive settings (authentication optional)
39    #[must_use]
40    pub fn permissive() -> Self {
41        Self::from_config(AuthConfig::permissive())
42    }
43
44    /// Create middleware with standard settings (authentication required)
45    #[must_use]
46    pub fn standard() -> Self {
47        Self::from_config(AuthConfig::standard())
48    }
49
50    /// Create middleware with strict settings (authentication required, short expiry)
51    #[must_use]
52    pub fn strict() -> Self {
53        Self::from_config(AuthConfig::strict())
54    }
55
56    /// Validate authentication in a request.
57    ///
58    /// Performs validation checks in order:
59    /// 1. Extract token from Authorization header
60    /// 2. Validate token signature (if signing key configured)
61    /// 3. Check token expiry (exp claim)
62    /// 4. Validate issuer/audience claims (if configured)
63    /// 5. Extract required claims (sub)
64    /// 6. Extract optional claims (scope, aud, iss)
65    ///
66    /// # Errors
67    ///
68    /// Returns [`SecurityError::AuthRequired`] if no Authorization header or
69    /// token is missing. Returns [`SecurityError::InvalidToken`] if the token
70    /// signature, expiry, issuer, or audience is invalid. Returns
71    /// [`SecurityError::TokenMissingClaim`] if a required claim is absent.
72    pub fn validate_request(&self, req: &AuthRequest) -> Result<AuthenticatedUser> {
73        // Check 1: Extract token from Authorization header
74        let token = self.extract_token(req)?;
75
76        // Check 2: Validate token (with or without signature verification)
77        if let Some(ref signing_key) = self.config.signing_key {
78            // Use jsonwebtoken crate for proper signature verification
79            self.validate_token_with_signature(&token, signing_key)
80        } else {
81            // Fallback: structure validation only (for testing/backwards compatibility)
82            // WARNING: This is insecure for production use!
83            self.validate_token_structure_only(&token)
84        }
85    }
86
87    /// Validate token with cryptographic signature verification.
88    ///
89    /// This is the secure path used when a signing key is configured.
90    fn validate_token_with_signature(
91        &self,
92        token: &str,
93        signing_key: &SigningKey,
94    ) -> Result<AuthenticatedUser> {
95        // Get the decoding key
96        let decoding_key = signing_key.to_decoding_key()?;
97
98        // Build validation configuration
99        let mut validation = Validation::new(signing_key.algorithm());
100
101        // Configure issuer validation (only validate if configured)
102        if let Some(ref issuer) = self.config.issuer {
103            validation.set_issuer(&[issuer]);
104        }
105        // Note: If issuer is not set, validation.iss is None and won't be validated
106
107        // Configure audience validation.
108        // SECURITY: `validate_aud = true` is the default in jsonwebtoken; we must
109        // NOT override it to `false` when no audience is configured, as that would
110        // silently accept tokens issued for any service (cross-service token replay).
111        // When no audience is pinned, any non-empty `aud` claim is accepted — callers
112        // should set `audience` in config to restrict this further.
113        if let Some(ref audience) = self.config.audience {
114            validation.set_audience(&[audience]);
115        }
116        // `validation.validate_aud` remains `true` (the library default) when no
117        // specific audience is configured.
118
119        // Set clock skew tolerance
120        validation.leeway = self.config.clock_skew_secs;
121
122        // Decode and validate the token
123        let token_data = decode::<JwtClaims>(token, &decoding_key, &validation).map_err(|e| {
124            match e.kind() {
125                jsonwebtoken::errors::ErrorKind::ExpiredSignature => {
126                    // Try to extract the actual expiry time from the token
127                    SecurityError::TokenExpired {
128                        expired_at: Utc::now(), // Approximate - actual time is not accessible
129                    }
130                },
131                jsonwebtoken::errors::ErrorKind::InvalidSignature => {
132                    SecurityError::JwtSignatureInvalid
133                },
134                jsonwebtoken::errors::ErrorKind::InvalidIssuer => {
135                    SecurityError::JwtIssuerMismatch {
136                        expected: self
137                            .config
138                            .issuer
139                            .clone()
140                            .unwrap_or_else(|| "(not configured)".to_string()),
141                    }
142                },
143                jsonwebtoken::errors::ErrorKind::InvalidAudience => {
144                    SecurityError::JwtAudienceMismatch {
145                        expected: self
146                            .config
147                            .audience
148                            .clone()
149                            .unwrap_or_else(|| "(not configured)".to_string()),
150                    }
151                },
152                jsonwebtoken::errors::ErrorKind::InvalidAlgorithm => {
153                    SecurityError::InvalidTokenAlgorithm {
154                        algorithm: format!("{:?}", signing_key.algorithm()),
155                    }
156                },
157                jsonwebtoken::errors::ErrorKind::MissingRequiredClaim(claim) => {
158                    SecurityError::TokenMissingClaim {
159                        claim: claim.clone(),
160                    }
161                },
162                _ => SecurityError::InvalidToken,
163            }
164        })?;
165
166        let claims = token_data.claims;
167
168        // Extract scopes (supports multiple formats)
169        let scopes = self.extract_scopes_from_jwt_claims(&claims);
170
171        // Extract user ID (required)
172        let user_id = claims.sub.ok_or(SecurityError::TokenMissingClaim {
173            claim: "sub".to_string(),
174        })?;
175
176        // Extract expiration (required)
177        let exp = claims.exp.ok_or(SecurityError::TokenMissingClaim {
178            claim: "exp".to_string(),
179        })?;
180
181        let expires_at =
182            DateTime::<Utc>::from_timestamp(exp, 0).ok_or(SecurityError::InvalidToken)?;
183
184        Ok(AuthenticatedUser {
185            user_id,
186            scopes,
187            expires_at,
188        })
189    }
190
191    /// Extract scopes from JWT claims.
192    ///
193    /// Supports multiple formats:
194    /// - `scope`: space-separated string (`OAuth2` standard)
195    /// - `scp`: array of strings (Microsoft)
196    /// - `permissions`: array of strings (Auth0 RBAC)
197    fn extract_scopes_from_jwt_claims(&self, claims: &JwtClaims) -> Vec<String> {
198        // Try space-separated scope string first (most common)
199        if let Some(ref scope) = claims.scope {
200            return scope.split_whitespace().map(String::from).collect();
201        }
202
203        // Try array of scopes (scp claim)
204        if let Some(ref scp) = claims.scp {
205            return scp.clone();
206        }
207
208        // Try permissions array (Auth0 RBAC)
209        if let Some(ref permissions) = claims.permissions {
210            return permissions.clone();
211        }
212
213        Vec::new()
214    }
215
216    /// Validate token structure only (no signature verification).
217    ///
218    /// WARNING: This is insecure and should only be used for testing
219    /// or when signature verification is handled elsewhere.
220    fn validate_token_structure_only(&self, token: &str) -> Result<AuthenticatedUser> {
221        // Validate basic structure
222        self.validate_token_structure(token)?;
223
224        // Parse claims
225        let claims = self.parse_claims(token)?;
226
227        // Extract and validate 'exp' claim (required)
228        let exp = claims.exp.ok_or(SecurityError::TokenMissingClaim {
229            claim: "exp".to_string(),
230        })?;
231
232        // Check expiry
233        let expires_at =
234            DateTime::<Utc>::from_timestamp(exp, 0).ok_or(SecurityError::InvalidToken)?;
235
236        if expires_at <= Utc::now() {
237            return Err(SecurityError::TokenExpired {
238                expired_at: expires_at,
239            });
240        }
241
242        // Extract and validate 'sub' claim (required)
243        let user_id = claims.sub.ok_or(SecurityError::TokenMissingClaim {
244            claim: "sub".to_string(),
245        })?;
246
247        // Extract optional claims
248        let scopes = claims
249            .scope
250            .as_ref()
251            .map(|s| s.split_whitespace().map(String::from).collect())
252            .unwrap_or_default();
253
254        Ok(AuthenticatedUser {
255            user_id,
256            scopes,
257            expires_at,
258        })
259    }
260
261    /// Extract token from the authorization header
262    fn extract_token(&self, req: &AuthRequest) -> Result<String> {
263        // If auth is not required and no header present, that's OK
264        if !self.config.required && req.authorization_header.is_none() {
265            return Err(SecurityError::AuthRequired); // Will be handled differently
266        }
267
268        req.extract_bearer_token()
269    }
270
271    /// Validate token structure (basic checks)
272    ///
273    /// A real implementation would validate the signature here.
274    /// For now, we just check basic structure.
275    fn validate_token_structure(&self, token: &str) -> Result<()> {
276        // JWT has 3 parts separated by dots: header.payload.signature
277        let parts: Vec<&str> = token.split('.').collect();
278        if parts.len() != 3 {
279            return Err(SecurityError::InvalidToken);
280        }
281
282        // Check that each part is non-empty
283        if parts.iter().any(|p| p.is_empty()) {
284            return Err(SecurityError::InvalidToken);
285        }
286
287        Ok(())
288    }
289
290    /// Parse JWT claims (simplified, for demo purposes)
291    ///
292    /// In a real implementation, this would decode and validate the JWT signature.
293    /// For testing, we accept a special test token format: "`test:{json_payload`}"
294    fn parse_claims(&self, token: &str) -> Result<TokenClaims> {
295        // Split the token
296        let parts: Vec<&str> = token.split('.').collect();
297        if parts.len() != 3 {
298            return Err(SecurityError::InvalidToken);
299        }
300
301        // For testing, we use a simple format: part1.{json}.part3
302        // where {json} is a base64-like encoded JSON
303        // Since we don't have base64 in core dependencies, we'll try to parse directly
304        let payload_part = parts[1];
305
306        // Try to decode as hex (simpler than base64 and no dependencies)
307        // For test tokens, we'll encode the JSON as hex
308        if let Ok(decoded) = hex::decode(payload_part) {
309            if let Ok(json_str) = std::str::from_utf8(&decoded) {
310                if let Ok(json) = serde_json::from_str::<serde_json::Value>(json_str) {
311                    return Ok(self.extract_claims_from_json(&json));
312                }
313            }
314        }
315
316        // If hex decoding fails, try to parse as UTF-8 directly (for test tokens created inline)
317        if let Ok(json) = serde_json::from_str::<serde_json::Value>(payload_part) {
318            return Ok(self.extract_claims_from_json(&json));
319        }
320
321        Err(SecurityError::InvalidToken)
322    }
323
324    /// Extract claims from parsed JSON
325    fn extract_claims_from_json(&self, json: &serde_json::Value) -> TokenClaims {
326        let sub = json["sub"].as_str().map(String::from);
327        let exp = json["exp"].as_i64();
328        let scope = json["scope"].as_str().map(String::from);
329        let aud = json["aud"]
330            .as_array()
331            .map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect());
332        let iss = json["iss"].as_str().map(String::from);
333
334        TokenClaims {
335            sub,
336            exp,
337            scope,
338            aud,
339            iss,
340        }
341    }
342
343    /// Get the underlying configuration
344    #[must_use]
345    pub const fn config(&self) -> &AuthConfig {
346        &self.config
347    }
348}
349
350#[cfg(test)]
351mod tests {
352    #![allow(clippy::unwrap_used)] // Reason: test code, panics are acceptable
353
354    use chrono::Utc;
355    use jsonwebtoken::Algorithm;
356    use zeroize::Zeroizing;
357
358    use super::{
359        super::{
360            config::AuthConfig,
361            signing_key::SigningKey,
362            types::{AuthRequest, AuthenticatedUser},
363        },
364        *,
365    };
366    use crate::security::errors::SecurityError;
367
368    // ============================================================================
369    // Helper Functions
370    // ============================================================================
371
372    /// Create a valid JWT token with specified claims (for testing)
373    ///
374    /// Note: This creates a structurally valid JWT, but doesn't sign it.
375    /// For real use, you'd use a proper JWT library.
376    fn create_test_token(sub: &str, exp_offset_secs: i64, scope: Option<&str>) -> String {
377        let now = chrono::Utc::now().timestamp();
378        let exp = now + exp_offset_secs;
379
380        // Create payload
381        let mut payload = serde_json::json!({
382            "sub": sub,
383            "exp": exp,
384            "iat": now,
385            "aud": ["test-audience"],
386            "iss": "test-issuer"
387        });
388
389        if let Some(s) = scope {
390            payload["scope"] = serde_json::json!(s);
391        }
392
393        // Encode payload as hex for testing
394        let payload_json = payload.to_string();
395        let payload_hex = hex::encode(&payload_json);
396        let signature = "test_signature"; // Not a real signature
397
398        // Format: header.payload_hex.signature
399        format!("header.{payload_hex}.{signature}")
400    }
401
402    // ============================================================================
403    // Check 1: Token Extraction Tests
404    // ============================================================================
405
406    #[test]
407    fn test_bearer_token_extracted_correctly() {
408        let token = "test_token_12345";
409        let req = AuthRequest::new(Some(format!("Bearer {token}")));
410
411        let extracted = req
412            .extract_bearer_token()
413            .unwrap_or_else(|e| panic!("expected bearer token extraction to succeed: {e}"));
414        assert_eq!(extracted, token);
415    }
416
417    #[test]
418    fn test_missing_authorization_header_rejected() {
419        let req = AuthRequest::new(None);
420
421        let result = req.extract_bearer_token();
422        assert!(matches!(result, Err(SecurityError::AuthRequired)));
423    }
424
425    #[test]
426    fn test_invalid_authorization_header_format_rejected() {
427        let req = AuthRequest::new(Some("Basic abc123".to_string()));
428
429        let result = req.extract_bearer_token();
430        assert!(matches!(result, Err(SecurityError::AuthRequired)));
431    }
432
433    #[test]
434    fn test_bearer_prefix_required() {
435        let req = AuthRequest::new(Some("abc123".to_string()));
436
437        let result = req.extract_bearer_token();
438        assert!(matches!(result, Err(SecurityError::AuthRequired)));
439    }
440
441    // ============================================================================
442    // Check 2: Token Structure Validation Tests
443    // ============================================================================
444
445    #[test]
446    fn test_valid_token_structure() {
447        let middleware = AuthMiddleware::permissive();
448        let token = create_test_token("user123", 3600, None);
449
450        let result = middleware.validate_token_structure(&token);
451        result.unwrap_or_else(|e| panic!("expected valid token structure: {e}"));
452    }
453
454    #[test]
455    fn test_token_with_wrong_part_count_rejected() {
456        let middleware = AuthMiddleware::permissive();
457        let token = "header.payload"; // Missing signature
458
459        let result = middleware.validate_token_structure(token);
460        assert!(matches!(result, Err(SecurityError::InvalidToken)));
461    }
462
463    #[test]
464    fn test_token_with_empty_part_rejected() {
465        let middleware = AuthMiddleware::permissive();
466        let token = "header..signature"; // Empty payload
467
468        let result = middleware.validate_token_structure(token);
469        assert!(matches!(result, Err(SecurityError::InvalidToken)));
470    }
471
472    // ============================================================================
473    // Check 3: Token Expiry Validation Tests
474    // ============================================================================
475
476    #[test]
477    fn test_valid_token_not_expired() {
478        let middleware = AuthMiddleware::standard();
479        let token = create_test_token("user123", 3600, None); // 1 hour from now
480        let req = AuthRequest::new(Some(format!("Bearer {token}")));
481
482        let result = middleware.validate_request(&req);
483        result.unwrap_or_else(|e| panic!("expected valid non-expired token to pass: {e}"));
484    }
485
486    #[test]
487    fn test_expired_token_rejected() {
488        let middleware = AuthMiddleware::standard();
489        let token = create_test_token("user123", -3600, None); // 1 hour ago
490        let req = AuthRequest::new(Some(format!("Bearer {token}")));
491
492        let result = middleware.validate_request(&req);
493        assert!(matches!(result, Err(SecurityError::TokenExpired { .. })));
494    }
495
496    /// Sentinel: a token that expired 1 second ago must be rejected.
497    ///
498    /// Kills the `<= → >` and `<= → never-expire` mutations on the expiry check:
499    /// `if expires_at <= Utc::now()`.
500    ///
501    /// Note: testing the exact `expires_at == now` boundary deterministically would
502    /// require clock injection into `AuthMiddleware`, which is not yet supported.
503    /// The ±1-second cases are tested here; the zero-offset case is inherently racy.
504    #[test]
505    fn test_token_expired_one_second_ago_is_rejected() {
506        let middleware = AuthMiddleware::standard();
507        let token = create_test_token("user123", -1, None); // expired 1 second ago
508        let req = AuthRequest::new(Some(format!("Bearer {token}")));
509
510        assert!(
511            matches!(middleware.validate_request(&req), Err(SecurityError::TokenExpired { .. })),
512            "token expired 1s ago must be rejected"
513        );
514    }
515
516    /// Sentinel: a token expiring 60 seconds from now must be accepted.
517    ///
518    /// Complements `test_token_expired_one_second_ago_is_rejected` to pin the valid
519    /// side of the expiry boundary.
520    #[test]
521    fn test_token_expiring_soon_is_accepted() {
522        let middleware = AuthMiddleware::standard();
523        let token = create_test_token("user123", 60, None); // expires in 60 seconds
524        let req = AuthRequest::new(Some(format!("Bearer {token}")));
525
526        assert!(
527            middleware.validate_request(&req).is_ok(),
528            "token expiring in 60s must be accepted"
529        );
530    }
531
532    // ============================================================================
533    // Check 4: Required Claims Validation Tests
534    // ============================================================================
535
536    #[test]
537    fn test_missing_sub_claim_rejected() {
538        let middleware = AuthMiddleware::standard();
539
540        // Create token without 'sub' claim
541        let now = chrono::Utc::now().timestamp();
542        let payload = serde_json::json!({
543            "exp": now + 3600,
544            "iat": now
545        });
546
547        let payload_hex = hex::encode(payload.to_string());
548        let token = format!("header.{payload_hex}.signature");
549
550        let req = AuthRequest::new(Some(format!("Bearer {token}")));
551        let result = middleware.validate_request(&req);
552
553        assert!(matches!(
554            result,
555            Err(SecurityError::TokenMissingClaim { claim })
556            if claim == "sub"
557        ));
558    }
559
560    #[test]
561    fn test_missing_exp_claim_rejected() {
562        let middleware = AuthMiddleware::standard();
563
564        // Create token without 'exp' claim
565        let payload = serde_json::json!({
566            "sub": "user123",
567            "iat": chrono::Utc::now().timestamp()
568        });
569
570        let payload_hex = hex::encode(payload.to_string());
571        let token = format!("header.{payload_hex}.signature");
572
573        let req = AuthRequest::new(Some(format!("Bearer {token}")));
574        let result = middleware.validate_request(&req);
575
576        assert!(matches!(
577            result,
578            Err(SecurityError::TokenMissingClaim { claim })
579            if claim == "exp"
580        ));
581    }
582
583    // ============================================================================
584    // Check 5: User Info Extraction Tests
585    // ============================================================================
586
587    #[test]
588    fn test_user_id_extracted_from_token() {
589        let middleware = AuthMiddleware::standard();
590        let token = create_test_token("user_12345", 3600, None);
591        let req = AuthRequest::new(Some(format!("Bearer {token}")));
592
593        let user = middleware
594            .validate_request(&req)
595            .unwrap_or_else(|e| panic!("expected user_id extraction to succeed: {e}"));
596        assert_eq!(user.user_id, "user_12345");
597    }
598
599    #[test]
600    fn test_scopes_extracted_from_token() {
601        let middleware = AuthMiddleware::standard();
602        let token = create_test_token("user123", 3600, Some("read write admin"));
603        let req = AuthRequest::new(Some(format!("Bearer {token}")));
604
605        let user = middleware
606            .validate_request(&req)
607            .unwrap_or_else(|e| panic!("expected scope extraction to succeed: {e}"));
608        assert_eq!(user.scopes, vec!["read", "write", "admin"]);
609    }
610
611    #[test]
612    fn test_empty_scopes_when_scope_claim_absent() {
613        let middleware = AuthMiddleware::standard();
614        let token = create_test_token("user123", 3600, None);
615        let req = AuthRequest::new(Some(format!("Bearer {token}")));
616
617        let user = middleware
618            .validate_request(&req)
619            .unwrap_or_else(|e| panic!("expected token without scopes to be valid: {e}"));
620        assert!(user.scopes.is_empty(), "expected empty scopes, got: {:?}", user.scopes);
621    }
622
623    #[test]
624    fn test_expires_at_extracted_correctly() {
625        let middleware = AuthMiddleware::standard();
626        let offset_secs = 7200; // 2 hours
627
628        let token = create_test_token("user123", offset_secs, None);
629        let req = AuthRequest::new(Some(format!("Bearer {token}")));
630
631        let user = middleware
632            .validate_request(&req)
633            .unwrap_or_else(|e| panic!("expected expiry extraction to succeed: {e}"));
634        let now = Utc::now();
635        let diff = (user.expires_at - now).num_seconds();
636
637        // Should be approximately offset_secs (within 5 seconds due to processing)
638        assert!((offset_secs - 5..=offset_secs + 5).contains(&diff));
639    }
640
641    // ============================================================================
642    // Configuration Tests
643    // ============================================================================
644
645    #[test]
646    fn test_permissive_config() {
647        let config = AuthConfig::permissive();
648        assert!(!config.required);
649        assert_eq!(config.token_expiry_secs, 3600);
650    }
651
652    #[test]
653    fn test_standard_config() {
654        let config = AuthConfig::standard();
655        assert!(config.required);
656        assert_eq!(config.token_expiry_secs, 3600);
657    }
658
659    #[test]
660    fn test_strict_config() {
661        let config = AuthConfig::strict();
662        assert!(config.required);
663        assert_eq!(config.token_expiry_secs, 1800);
664    }
665
666    #[test]
667    fn test_middleware_helpers() {
668        let permissive = AuthMiddleware::permissive();
669        assert!(!permissive.config().required);
670
671        let standard = AuthMiddleware::standard();
672        assert!(standard.config().required);
673
674        let strict = AuthMiddleware::strict();
675        assert!(strict.config().required);
676    }
677
678    // ============================================================================
679    // AuthenticatedUser Tests
680    // ============================================================================
681
682    #[test]
683    fn test_user_has_scope() {
684        let user = AuthenticatedUser {
685            user_id:    "user123".to_string(),
686            scopes:     vec!["read".to_string(), "write".to_string()],
687            expires_at: Utc::now() + chrono::Duration::hours(1),
688        };
689
690        assert!(user.has_scope("read"));
691        assert!(user.has_scope("write"));
692        assert!(!user.has_scope("admin"));
693    }
694
695    #[test]
696    fn test_user_is_not_expired() {
697        let user = AuthenticatedUser {
698            user_id:    "user123".to_string(),
699            scopes:     vec![],
700            expires_at: Utc::now() + chrono::Duration::hours(1),
701        };
702
703        assert!(!user.is_expired());
704    }
705
706    #[test]
707    fn test_user_is_expired() {
708        let user = AuthenticatedUser {
709            user_id:    "user123".to_string(),
710            scopes:     vec![],
711            expires_at: Utc::now() - chrono::Duration::hours(1),
712        };
713
714        assert!(user.is_expired());
715    }
716
717    #[test]
718    fn test_user_ttl_calculation() {
719        let now = Utc::now();
720        let expires_at = now + chrono::Duration::hours(2);
721        let user = AuthenticatedUser {
722            user_id: "user123".to_string(),
723            scopes: vec![],
724            expires_at,
725        };
726
727        let ttl = user.ttl_secs();
728        // Should be approximately 7200 seconds (2 hours)
729        assert!((7195..=7205).contains(&ttl));
730    }
731
732    #[test]
733    fn test_user_display() {
734        let user = AuthenticatedUser {
735            user_id:    "user123".to_string(),
736            scopes:     vec![],
737            expires_at: Utc::now() + chrono::Duration::hours(1),
738        };
739
740        let display_str = user.to_string();
741        assert!(display_str.contains("user123"));
742        assert!(display_str.contains("UTC"));
743    }
744
745    // ============================================================================
746    // Error Message Tests
747    // ============================================================================
748
749    #[test]
750    fn test_error_messages_clear_and_actionable() {
751        let middleware = AuthMiddleware::standard();
752
753        // Test missing header error
754        let req = AuthRequest::new(None);
755        let result = middleware.validate_request(&req);
756        assert!(matches!(result, Err(SecurityError::AuthRequired)));
757
758        // Test invalid format error
759        let req = AuthRequest::new(Some("Basic xyz".to_string()));
760        let result = middleware.validate_request(&req);
761        assert!(matches!(result, Err(SecurityError::AuthRequired)));
762    }
763
764    // ============================================================================
765    // Edge Cases
766    // ============================================================================
767
768    #[test]
769    fn test_auth_not_required_allows_missing_token() {
770        // When auth is NOT required, missing token should still go through extraction
771        let middleware = AuthMiddleware::permissive(); // required = false
772        let req = AuthRequest::new(None);
773
774        let result = middleware.validate_request(&req);
775        // Should fail at extraction, not because auth is optional
776        assert!(matches!(result, Err(SecurityError::AuthRequired)));
777    }
778
779    #[test]
780    fn test_whitespace_in_scopes_handled() {
781        let middleware = AuthMiddleware::standard();
782        let token = create_test_token("user123", 3600, Some("  read   write  admin  "));
783        let req = AuthRequest::new(Some(format!("Bearer {token}")));
784
785        let user = middleware
786            .validate_request(&req)
787            .unwrap_or_else(|e| panic!("expected whitespace-heavy scopes to parse: {e}"));
788        // split_whitespace handles multiple spaces correctly
789        assert_eq!(user.scopes.len(), 3);
790    }
791
792    #[test]
793    fn test_single_scope_parsed_correctly() {
794        let middleware = AuthMiddleware::standard();
795        let token = create_test_token("user123", 3600, Some("read"));
796        let req = AuthRequest::new(Some(format!("Bearer {token}")));
797
798        let user = middleware
799            .validate_request(&req)
800            .unwrap_or_else(|e| panic!("expected single scope to parse: {e}"));
801        assert_eq!(user.scopes, vec!["read"]);
802    }
803
804    // ============================================================================
805    // JWT Signature Verification Tests (Issue #225)
806    // ============================================================================
807
808    /// Helper to create a properly signed HS256 JWT token
809    #[allow(clippy::items_after_statements)] // Reason: test helper structs defined near point of use for readability
810    fn create_signed_hs256_token(
811        sub: &str,
812        exp_offset_secs: i64,
813        scope: Option<&str>,
814        secret: &str,
815    ) -> String {
816        use jsonwebtoken::{EncodingKey, Header, encode};
817
818        let now = chrono::Utc::now().timestamp();
819        let exp = now + exp_offset_secs;
820
821        #[derive(serde::Serialize)]
822        struct Claims {
823            sub:   String,
824            exp:   i64,
825            iat:   i64,
826            #[serde(skip_serializing_if = "Option::is_none")]
827            scope: Option<String>,
828        }
829
830        let claims = Claims {
831            sub: sub.to_string(),
832            exp,
833            iat: now,
834            scope: scope.map(String::from),
835        };
836
837        encode(
838            &Header::default(), // HS256
839            &claims,
840            &EncodingKey::from_secret(secret.as_bytes()),
841        )
842        .expect("Failed to create test token")
843    }
844
845    #[test]
846    fn test_hs256_signature_verification_valid_token() {
847        let secret = "super-secret-key-for-testing-only";
848        let config = AuthConfig::with_hs256(secret);
849        let middleware = AuthMiddleware::from_config(config);
850
851        let token = create_signed_hs256_token("user123", 3600, Some("read write"), secret);
852        let req = AuthRequest::new(Some(format!("Bearer {token}")));
853
854        let result = middleware.validate_request(&req);
855        assert!(result.is_ok(), "Expected valid token, got: {:?}", result);
856
857        let user = result.unwrap();
858        assert_eq!(user.user_id, "user123");
859        assert_eq!(user.scopes, vec!["read", "write"]);
860    }
861
862    #[test]
863    fn test_hs256_signature_verification_wrong_secret_rejected() {
864        let signing_secret = "correct-secret";
865        let wrong_secret = "wrong-secret";
866
867        let config = AuthConfig::with_hs256(signing_secret);
868        let middleware = AuthMiddleware::from_config(config);
869
870        // Token signed with wrong secret
871        let token = create_signed_hs256_token("user123", 3600, None, wrong_secret);
872        let req = AuthRequest::new(Some(format!("Bearer {token}")));
873
874        let result = middleware.validate_request(&req);
875        assert!(
876            matches!(result, Err(SecurityError::JwtSignatureInvalid)),
877            "Expected JwtSignatureInvalid for wrong signature, got: {:?}",
878            result
879        );
880    }
881
882    #[test]
883    fn test_hs256_expired_token_rejected() {
884        let secret = "test-secret";
885        let config = AuthConfig::with_hs256(secret);
886        let middleware = AuthMiddleware::from_config(config);
887
888        // Token expired 1 hour ago
889        let token = create_signed_hs256_token("user123", -3600, None, secret);
890        let req = AuthRequest::new(Some(format!("Bearer {token}")));
891
892        let result = middleware.validate_request(&req);
893        assert!(
894            matches!(result, Err(SecurityError::TokenExpired { .. })),
895            "Expected TokenExpired, got: {:?}",
896            result
897        );
898    }
899
900    #[test]
901    #[allow(clippy::items_after_statements)] // Reason: test helper structs defined near point of use for readability
902    fn test_hs256_with_issuer_validation() {
903        use jsonwebtoken::{EncodingKey, Header, encode};
904
905        #[derive(serde::Serialize)]
906        struct ClaimsWithIss {
907            sub: String,
908            exp: i64,
909            iss: String,
910        }
911
912        let secret = "test-secret";
913        let config = AuthConfig::with_hs256(secret).with_issuer("https://auth.example.com");
914        let middleware = AuthMiddleware::from_config(config);
915
916        // Create token with matching issuer
917        let now = chrono::Utc::now().timestamp();
918        let claims = ClaimsWithIss {
919            sub: "user123".to_string(),
920            exp: now + 3600,
921            iss: "https://auth.example.com".to_string(),
922        };
923
924        let token =
925            encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes()))
926                .unwrap();
927
928        let req = AuthRequest::new(Some(format!("Bearer {token}")));
929        let result = middleware.validate_request(&req);
930        assert!(result.is_ok(), "Expected valid token with issuer, got: {:?}", result);
931    }
932
933    #[test]
934    #[allow(clippy::items_after_statements)] // Reason: test helper structs defined near point of use for readability
935    fn test_hs256_with_wrong_issuer_rejected() {
936        use jsonwebtoken::{EncodingKey, Header, encode};
937
938        #[derive(serde::Serialize)]
939        struct ClaimsWithIss {
940            sub: String,
941            exp: i64,
942            iss: String,
943        }
944
945        let secret = "test-secret";
946        let config = AuthConfig::with_hs256(secret).with_issuer("https://auth.example.com");
947        let middleware = AuthMiddleware::from_config(config);
948
949        // Create token with wrong issuer
950        let now = chrono::Utc::now().timestamp();
951        let claims = ClaimsWithIss {
952            sub: "user123".to_string(),
953            exp: now + 3600,
954            iss: "https://wrong-issuer.com".to_string(),
955        };
956
957        let token =
958            encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes()))
959                .unwrap();
960
961        let req = AuthRequest::new(Some(format!("Bearer {token}")));
962        let result = middleware.validate_request(&req);
963        assert!(
964            matches!(result, Err(SecurityError::JwtIssuerMismatch { .. })),
965            "Expected JwtIssuerMismatch for wrong issuer, got: {:?}",
966            result
967        );
968    }
969
970    #[test]
971    #[allow(clippy::items_after_statements)] // Reason: test helper structs defined near point of use for readability
972    fn test_jwt_issuer_mismatch_error_contains_expected_issuer_and_word_issuer() {
973        use jsonwebtoken::{EncodingKey, Header, encode};
974
975        #[derive(serde::Serialize)]
976        struct ClaimsWithIss {
977            sub: String,
978            exp: i64,
979            iss: String,
980        }
981
982        let secret = "test-secret";
983        let expected_issuer = "https://auth.example.com";
984        let config = AuthConfig::with_hs256(secret).with_issuer(expected_issuer);
985        let middleware = AuthMiddleware::from_config(config);
986
987        // Token signed with the right secret but wrong issuer.
988        let now = chrono::Utc::now().timestamp();
989        let claims = ClaimsWithIss {
990            sub: "user123".to_string(),
991            exp: now + 3600,
992            iss: "https://wrong-issuer.com".to_string(),
993        };
994
995        let token =
996            encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes()))
997                .unwrap();
998
999        let req = AuthRequest::new(Some(format!("Bearer {token}")));
1000        let result = middleware.validate_request(&req);
1001        let err = result.expect_err("expected error for wrong issuer");
1002        let msg = err.to_string();
1003
1004        assert!(
1005            msg.contains(expected_issuer),
1006            "error message must contain expected issuer '{expected_issuer}': {msg}"
1007        );
1008        assert!(msg.contains("issuer"), "error message must contain 'issuer': {msg}");
1009    }
1010
1011    #[test]
1012    #[allow(clippy::items_after_statements)] // Reason: test helper structs defined near point of use for readability
1013    fn test_hs256_with_audience_validation() {
1014        use jsonwebtoken::{EncodingKey, Header, encode};
1015
1016        #[derive(serde::Serialize)]
1017        struct ClaimsWithAud {
1018            sub: String,
1019            exp: i64,
1020            aud: String,
1021        }
1022
1023        let secret = "test-secret";
1024        let config = AuthConfig::with_hs256(secret).with_audience("my-api");
1025        let middleware = AuthMiddleware::from_config(config);
1026
1027        // Create token with matching audience
1028        let now = chrono::Utc::now().timestamp();
1029        let claims = ClaimsWithAud {
1030            sub: "user123".to_string(),
1031            exp: now + 3600,
1032            aud: "my-api".to_string(),
1033        };
1034
1035        let token =
1036            encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes()))
1037                .unwrap();
1038
1039        let req = AuthRequest::new(Some(format!("Bearer {token}")));
1040        let result = middleware.validate_request(&req);
1041        assert!(result.is_ok(), "Expected valid token with audience, got: {:?}", result);
1042    }
1043
1044    #[test]
1045    fn test_signing_key_algorithm_detection() {
1046        let hs256 = SigningKey::hs256("secret");
1047        assert!(matches!(hs256.algorithm(), Algorithm::HS256));
1048
1049        let hs384 = SigningKey::Hs384(Zeroizing::new(b"secret".to_vec()));
1050        assert!(matches!(hs384.algorithm(), Algorithm::HS384));
1051
1052        let hs512 = SigningKey::Hs512(Zeroizing::new(b"secret".to_vec()));
1053        assert!(matches!(hs512.algorithm(), Algorithm::HS512));
1054
1055        let rs256_pem = SigningKey::rs256_pem("fake-pem");
1056        assert!(matches!(rs256_pem.algorithm(), Algorithm::RS256));
1057
1058        let rs256_comp = SigningKey::rs256_components("n", "e");
1059        assert!(matches!(rs256_comp.algorithm(), Algorithm::RS256));
1060    }
1061
1062    #[test]
1063    fn test_config_has_signing_key() {
1064        let config_without = AuthConfig::standard();
1065        assert!(!config_without.has_signing_key());
1066
1067        let config_with = AuthConfig::with_hs256("secret");
1068        assert!(config_with.has_signing_key());
1069    }
1070
1071    #[test]
1072    fn test_config_builder_pattern() {
1073        let config = AuthConfig::with_hs256("secret")
1074            .with_issuer("https://auth.example.com")
1075            .with_audience("my-api");
1076
1077        assert!(config.has_signing_key());
1078        assert_eq!(config.issuer, Some("https://auth.example.com".to_string()));
1079        assert_eq!(config.audience, Some("my-api".to_string()));
1080    }
1081
1082    #[test]
1083    fn test_malformed_token_rejected_with_signature_verification() {
1084        let config = AuthConfig::with_hs256("secret");
1085        let middleware = AuthMiddleware::from_config(config);
1086
1087        // Not a valid JWT at all
1088        let req = AuthRequest::new(Some("Bearer not-a-jwt".to_string()));
1089        let result = middleware.validate_request(&req);
1090        assert!(
1091            matches!(result, Err(SecurityError::InvalidToken)),
1092            "Expected InvalidToken for malformed JWT, got: {:?}",
1093            result
1094        );
1095    }
1096
1097    #[test]
1098    fn test_tampered_payload_rejected() {
1099        let secret = "test-secret";
1100        let config = AuthConfig::with_hs256(secret);
1101        let middleware = AuthMiddleware::from_config(config);
1102
1103        // Create a valid token
1104        let token = create_signed_hs256_token("user123", 3600, None, secret);
1105
1106        // Tamper with the payload (change middle part)
1107        let parts: Vec<&str> = token.split('.').collect();
1108        let tampered_token = format!("{}.dGFtcGVyZWQ.{}", parts[0], parts[2]);
1109
1110        let req = AuthRequest::new(Some(format!("Bearer {tampered_token}")));
1111        let result = middleware.validate_request(&req);
1112        assert!(
1113            matches!(result, Err(SecurityError::JwtSignatureInvalid)),
1114            "Expected JwtSignatureInvalid for tampered payload, got: {:?}",
1115            result
1116        );
1117    }
1118
1119    #[test]
1120    fn test_clock_skew_tolerance() {
1121        let secret = "test-secret";
1122        let mut config = AuthConfig::with_hs256(secret);
1123        config.clock_skew_secs = 120; // 2 minutes tolerance
1124        let middleware = AuthMiddleware::from_config(config);
1125
1126        // Token that expired 30 seconds ago (within 2 minute tolerance)
1127        let token = create_signed_hs256_token("user123", -30, None, secret);
1128        let req = AuthRequest::new(Some(format!("Bearer {token}")));
1129
1130        let result = middleware.validate_request(&req);
1131        // Should still be valid due to clock skew tolerance
1132        assert!(result.is_ok(), "Expected valid token within clock skew, got: {:?}", result);
1133    }
1134}