Skip to main content

amaters_server/
auth.rs

1//! Authentication module
2//!
3//! This module provides authentication services for the server:
4//! - mTLS (Mutual TLS) client certificate validation
5//! - JWT (JSON Web Token) authentication
6//! - API key authentication
7//!
8//! Security model:
9//! - Secure by default (deny unless explicitly allowed)
10//! - Multiple authentication methods can be enabled simultaneously
11//! - Authentication results in a validated identity (Principal)
12
13use crate::config::{ApiKeySettings, AuthSettings, JwtSettings, MtlsSettings};
14use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
15use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode, decode_header};
16use serde::{Deserialize, Serialize};
17use sha2::{Digest, Sha256};
18use std::collections::HashMap;
19use std::fs;
20use std::path::Path;
21use std::sync::Arc;
22use thiserror::Error;
23use tracing::{debug, info, warn};
24use x509_parser::prelude::*;
25
26/// Authentication errors
27#[derive(Error, Debug)]
28pub enum AuthError {
29    #[error("Authentication failed: {0}")]
30    AuthenticationFailed(String),
31
32    #[error("Invalid credentials")]
33    InvalidCredentials,
34
35    #[error("Certificate validation failed: {0}")]
36    CertificateError(String),
37
38    #[error("JWT validation failed: {0}")]
39    JwtError(String),
40
41    #[error("API key validation failed: {0}")]
42    ApiKeyError(String),
43
44    #[error("Configuration error: {0}")]
45    ConfigError(String),
46
47    #[error("IO error: {0}")]
48    Io(#[from] std::io::Error),
49
50    #[error("JSON error: {0}")]
51    Json(#[from] serde_json::Error),
52
53    #[error("No authentication provided")]
54    NoAuthProvided,
55
56    #[error("Authentication method not enabled: {0}")]
57    MethodNotEnabled(String),
58}
59
60pub type AuthResult<T> = Result<T, AuthError>;
61
62/// Authenticated principal (user identity)
63#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct Principal {
65    /// Unique identifier for the user
66    pub id: String,
67
68    /// Username or common name
69    pub name: String,
70
71    /// Authentication method used
72    pub auth_method: AuthMethod,
73
74    /// Additional attributes (roles, groups, etc.)
75    pub attributes: HashMap<String, String>,
76}
77
78impl Principal {
79    /// Create a new principal
80    pub fn new(id: String, name: String, auth_method: AuthMethod) -> Self {
81        Self {
82            id,
83            name,
84            auth_method,
85            attributes: HashMap::new(),
86        }
87    }
88
89    /// Add an attribute to the principal
90    pub fn with_attribute(mut self, key: String, value: String) -> Self {
91        self.attributes.insert(key, value);
92        self
93    }
94
95    /// Get an attribute value
96    pub fn get_attribute(&self, key: &str) -> Option<&String> {
97        self.attributes.get(key)
98    }
99
100    /// Check if principal has a specific role
101    pub fn has_role(&self, role: &str) -> bool {
102        self.get_attribute("role")
103            .map(|r| r == role)
104            .unwrap_or(false)
105    }
106}
107
108/// Authentication method used
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
110pub enum AuthMethod {
111    /// mTLS client certificate
112    MutualTls,
113    /// JWT token
114    Jwt,
115    /// API key
116    ApiKey,
117}
118
119impl std::fmt::Display for AuthMethod {
120    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        match self {
122            AuthMethod::MutualTls => write!(f, "mTLS"),
123            AuthMethod::Jwt => write!(f, "JWT"),
124            AuthMethod::ApiKey => write!(f, "API Key"),
125        }
126    }
127}
128
129/// JWT claims structure
130#[derive(Debug, Serialize, Deserialize)]
131struct JwtClaims {
132    /// Subject (user ID)
133    sub: String,
134    /// Expiration time
135    exp: usize,
136    /// Issued at
137    iat: Option<usize>,
138    /// Issuer
139    iss: Option<String>,
140    /// Audience
141    aud: Option<String>,
142    /// User name
143    name: Option<String>,
144    /// Roles
145    roles: Option<Vec<String>>,
146    /// Custom attributes
147    #[serde(flatten)]
148    attributes: HashMap<String, serde_json::Value>,
149}
150
151/// API key entry
152#[derive(Debug, Clone, Serialize, Deserialize)]
153struct ApiKeyEntry {
154    /// Key ID
155    id: String,
156    /// Key name/description
157    name: String,
158    /// Hashed key value (if hashing enabled)
159    #[serde(skip_serializing_if = "Option::is_none")]
160    key_hash: Option<String>,
161    /// Plain key value (if hashing disabled)
162    #[serde(skip_serializing_if = "Option::is_none")]
163    key: Option<String>,
164    /// User ID
165    user_id: String,
166    /// Roles
167    #[serde(default)]
168    roles: Vec<String>,
169    /// Additional attributes
170    #[serde(default)]
171    attributes: HashMap<String, String>,
172}
173
174/// Authentication service
175pub struct Authenticator {
176    config: Arc<AuthSettings>,
177    mtls_validator: Option<MtlsValidator>,
178    jwt_validator: Option<JwtValidator>,
179    api_key_validator: Option<ApiKeyValidator>,
180}
181
182impl Authenticator {
183    /// Create a new authenticator
184    pub fn new(config: AuthSettings) -> AuthResult<Self> {
185        let config = Arc::new(config);
186
187        // Initialize mTLS validator
188        let mtls_validator = if config.mtls.enabled {
189            Some(MtlsValidator::new(config.mtls.clone())?)
190        } else {
191            None
192        };
193
194        // Initialize JWT validator
195        let jwt_validator = if config.jwt.enabled {
196            Some(JwtValidator::new(config.jwt.clone())?)
197        } else {
198            None
199        };
200
201        // Initialize API key validator
202        let api_key_validator = if config.api_key.enabled {
203            Some(ApiKeyValidator::new(config.api_key.clone())?)
204        } else {
205            None
206        };
207
208        Ok(Self {
209            config,
210            mtls_validator,
211            jwt_validator,
212            api_key_validator,
213        })
214    }
215
216    /// Authenticate using client certificate
217    pub fn authenticate_certificate(&self, cert_der: &[u8]) -> AuthResult<Principal> {
218        if !self.config.methods.contains(&"mtls".to_string()) {
219            return Err(AuthError::MethodNotEnabled("mTLS".to_string()));
220        }
221
222        let validator = self
223            .mtls_validator
224            .as_ref()
225            .ok_or_else(|| AuthError::MethodNotEnabled("mTLS".to_string()))?;
226
227        validator.validate_certificate(cert_der)
228    }
229
230    /// Authenticate using JWT token
231    pub fn authenticate_jwt(&self, token: &str) -> AuthResult<Principal> {
232        if !self.config.methods.contains(&"jwt".to_string()) {
233            return Err(AuthError::MethodNotEnabled("JWT".to_string()));
234        }
235
236        let validator = self
237            .jwt_validator
238            .as_ref()
239            .ok_or_else(|| AuthError::MethodNotEnabled("JWT".to_string()))?;
240
241        validator.validate_token(token)
242    }
243
244    /// Authenticate using API key
245    pub fn authenticate_api_key(&self, key: &str) -> AuthResult<Principal> {
246        if !self.config.methods.contains(&"api_key".to_string()) {
247            return Err(AuthError::MethodNotEnabled("API Key".to_string()));
248        }
249
250        let validator = self
251            .api_key_validator
252            .as_ref()
253            .ok_or_else(|| AuthError::MethodNotEnabled("API Key".to_string()))?;
254
255        validator.validate_key(key)
256    }
257
258    /// Check if authentication is enabled
259    pub fn is_enabled(&self) -> bool {
260        self.config.enabled
261    }
262
263    /// Check if a specific method is enabled
264    pub fn is_method_enabled(&self, method: &str) -> bool {
265        self.config.methods.contains(&method.to_string())
266    }
267}
268
269/// mTLS certificate validator
270struct MtlsValidator {
271    config: MtlsSettings,
272    ca_certs: Vec<Vec<u8>>,
273}
274
275impl MtlsValidator {
276    fn new(config: MtlsSettings) -> AuthResult<Self> {
277        let ca_certs = if let Some(ref ca_dir) = config.ca_certs_dir {
278            Self::load_ca_certificates(ca_dir)?
279        } else {
280            Vec::new()
281        };
282
283        Ok(Self { config, ca_certs })
284    }
285
286    fn load_ca_certificates(dir: &Path) -> AuthResult<Vec<Vec<u8>>> {
287        let mut certs = Vec::new();
288
289        if !dir.exists() {
290            return Err(AuthError::ConfigError(format!(
291                "CA certificates directory does not exist: {}",
292                dir.display()
293            )));
294        }
295
296        for entry_result in fs::read_dir(dir)? {
297            let entry = entry_result?;
298            let path = entry.path();
299
300            if path.is_file() {
301                if let Some(ext) = path.extension() {
302                    if ext == "crt" || ext == "pem" || ext == "der" {
303                        let cert_data = fs::read(&path)?;
304                        certs.push(cert_data);
305                        debug!("Loaded CA certificate: {}", path.display());
306                    }
307                }
308            }
309        }
310
311        info!("Loaded {} CA certificates", certs.len());
312        Ok(certs)
313    }
314
315    fn validate_certificate(&self, cert_der: &[u8]) -> AuthResult<Principal> {
316        // Parse the certificate
317        let (_, cert) = X509Certificate::from_der(cert_der).map_err(|e| {
318            AuthError::CertificateError(format!("Failed to parse certificate: {}", e))
319        })?;
320
321        // Verify certificate validity period
322        let now = std::time::SystemTime::now();
323        let not_before = cert.validity().not_before.to_datetime();
324        let not_after = cert.validity().not_after.to_datetime();
325
326        if now < not_before {
327            return Err(AuthError::CertificateError(
328                "Certificate not yet valid".to_string(),
329            ));
330        }
331
332        if now > not_after {
333            return Err(AuthError::CertificateError(
334                "Certificate has expired".to_string(),
335            ));
336        }
337
338        // Extract subject information
339        let subject = cert.subject();
340        let cn = subject
341            .iter_common_name()
342            .next()
343            .and_then(|cn| cn.as_str().ok())
344            .ok_or_else(|| AuthError::CertificateError("No CN in certificate".to_string()))?;
345
346        let organization = subject
347            .iter_organization()
348            .next()
349            .and_then(|o| o.as_str().ok());
350
351        // Verify organization if restrictions are configured
352        if !self.config.allowed_organizations.is_empty() {
353            let org = organization.ok_or_else(|| {
354                AuthError::CertificateError("Certificate has no organization".to_string())
355            })?;
356
357            if !self.config.allowed_organizations.contains(&org.to_string()) {
358                return Err(AuthError::CertificateError(format!(
359                    "Organization '{}' not allowed",
360                    org
361                )));
362            }
363        }
364
365        // Create principal
366        let mut principal = Principal::new(cn.to_string(), cn.to_string(), AuthMethod::MutualTls);
367
368        if let Some(org) = organization {
369            principal = principal.with_attribute("organization".to_string(), org.to_string());
370        }
371
372        debug!("Successfully authenticated certificate for user: {}", cn);
373        Ok(principal)
374    }
375}
376
377/// JWT token validator
378struct JwtValidator {
379    config: JwtSettings,
380    decoding_key: DecodingKey,
381    validation: Validation,
382}
383
384impl std::fmt::Debug for JwtValidator {
385    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386        f.debug_struct("JwtValidator")
387            .field("config", &self.config)
388            .field("decoding_key", &"<redacted>")
389            .field("validation", &"<validation>")
390            .finish()
391    }
392}
393
394impl JwtValidator {
395    fn new(config: JwtSettings) -> AuthResult<Self> {
396        let algorithm = match config.algorithm.as_str() {
397            "HS256" => Algorithm::HS256,
398            "HS384" => Algorithm::HS384,
399            "HS512" => Algorithm::HS512,
400            "RS256" => Algorithm::RS256,
401            "RS384" => Algorithm::RS384,
402            "RS512" => Algorithm::RS512,
403            "ES256" => Algorithm::ES256,
404            "ES384" => Algorithm::ES384,
405            "EdDSA" => Algorithm::EdDSA,
406            _ => {
407                return Err(AuthError::ConfigError(format!(
408                    "Unsupported JWT algorithm: {}",
409                    config.algorithm
410                )));
411            }
412        };
413
414        let decoding_key = match algorithm {
415            Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => {
416                let secret = config.secret.as_ref().ok_or_else(|| {
417                    AuthError::ConfigError("JWT secret not configured".to_string())
418                })?;
419                DecodingKey::from_secret(secret.as_bytes())
420            }
421            Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 => {
422                let public_key_path = config.public_key_path.as_ref().ok_or_else(|| {
423                    AuthError::ConfigError("JWT RSA public key path not configured".to_string())
424                })?;
425                let pem = fs::read_to_string(public_key_path)?;
426                DecodingKey::from_rsa_pem(pem.as_bytes()).map_err(|e| {
427                    AuthError::ConfigError(format!("Failed to load RSA public key: {}", e))
428                })?
429            }
430            Algorithm::ES256 | Algorithm::ES384 => {
431                let ec_key_path = config.ec_public_key_path.as_ref().ok_or_else(|| {
432                    AuthError::ConfigError("JWT EC public key path not configured".to_string())
433                })?;
434                let pem = fs::read_to_string(ec_key_path)?;
435                DecodingKey::from_ec_pem(pem.as_bytes()).map_err(|e| {
436                    AuthError::ConfigError(format!("Failed to load EC public key: {}", e))
437                })?
438            }
439            Algorithm::EdDSA => {
440                let ed_key_path = config.ed_public_key_path.as_ref().ok_or_else(|| {
441                    AuthError::ConfigError("JWT Ed25519 public key path not configured".to_string())
442                })?;
443                let pem = fs::read_to_string(ed_key_path)?;
444                DecodingKey::from_ed_pem(pem.as_bytes()).map_err(|e| {
445                    AuthError::ConfigError(format!("Failed to load Ed25519 public key: {}", e))
446                })?
447            }
448            _ => {
449                return Err(AuthError::ConfigError(
450                    "Algorithm not implemented".to_string(),
451                ));
452            }
453        };
454
455        let mut validation = Validation::new(algorithm);
456        validation.validate_exp = true;
457
458        if let Some(ref issuer) = config.issuer {
459            validation.set_issuer(&[issuer]);
460        }
461
462        if let Some(ref audience) = config.audience {
463            validation.set_audience(&[audience]);
464        }
465
466        Ok(Self {
467            config,
468            decoding_key,
469            validation,
470        })
471    }
472
473    fn validate_token(&self, token: &str) -> AuthResult<Principal> {
474        // Decode and validate the token
475        let token_data = decode::<JwtClaims>(token, &self.decoding_key, &self.validation)
476            .map_err(|e| AuthError::JwtError(format!("Token validation failed: {}", e)))?;
477
478        let claims = token_data.claims;
479
480        // Create principal
481        let name = claims.name.unwrap_or_else(|| claims.sub.clone());
482        let mut principal = Principal::new(claims.sub, name, AuthMethod::Jwt);
483
484        // Add roles
485        if let Some(roles) = claims.roles {
486            principal = principal.with_attribute("roles".to_string(), roles.join(","));
487        }
488
489        // Add custom attributes
490        for (key, value) in claims.attributes {
491            if let Some(s) = value.as_str() {
492                principal = principal.with_attribute(key, s.to_string());
493            }
494        }
495
496        debug!(
497            "Successfully authenticated JWT token for user: {}",
498            principal.name
499        );
500        Ok(principal)
501    }
502}
503
504/// API key validator
505struct ApiKeyValidator {
506    config: ApiKeySettings,
507    keys: HashMap<String, ApiKeyEntry>,
508}
509
510impl ApiKeyValidator {
511    fn new(config: ApiKeySettings) -> AuthResult<Self> {
512        let keys_file = config
513            .keys_file
514            .as_ref()
515            .ok_or_else(|| AuthError::ConfigError("API keys file not configured".to_string()))?;
516
517        let keys = Self::load_keys(keys_file, config.hash_keys)?;
518
519        info!("Loaded {} API keys", keys.len());
520
521        Ok(Self { config, keys })
522    }
523
524    fn load_keys(path: &Path, hash_keys: bool) -> AuthResult<HashMap<String, ApiKeyEntry>> {
525        if !path.exists() {
526            return Err(AuthError::ConfigError(format!(
527                "API keys file does not exist: {}",
528                path.display()
529            )));
530        }
531
532        let contents = fs::read_to_string(path)?;
533        let entries: Vec<ApiKeyEntry> = serde_json::from_str(&contents)?;
534
535        let mut keys = HashMap::new();
536        for entry in entries {
537            let key_value = if hash_keys {
538                entry
539                    .key_hash
540                    .clone()
541                    .ok_or_else(|| AuthError::ConfigError("Missing key_hash".to_string()))?
542            } else {
543                entry
544                    .key
545                    .clone()
546                    .ok_or_else(|| AuthError::ConfigError("Missing key".to_string()))?
547            };
548
549            keys.insert(key_value, entry);
550        }
551
552        Ok(keys)
553    }
554
555    fn validate_key(&self, key: &str) -> AuthResult<Principal> {
556        let lookup_key = if self.config.hash_keys {
557            Self::hash_key(key)
558        } else {
559            key.to_string()
560        };
561
562        let entry = self
563            .keys
564            .get(&lookup_key)
565            .ok_or(AuthError::InvalidCredentials)?;
566
567        // Create principal
568        let mut principal = Principal::new(
569            entry.user_id.clone(),
570            entry.name.clone(),
571            AuthMethod::ApiKey,
572        );
573
574        // Add roles
575        if !entry.roles.is_empty() {
576            principal = principal.with_attribute("roles".to_string(), entry.roles.join(","));
577        }
578
579        // Add custom attributes
580        for (key, value) in &entry.attributes {
581            principal = principal.with_attribute(key.clone(), value.clone());
582        }
583
584        debug!(
585            "Successfully authenticated API key for user: {}",
586            entry.user_id
587        );
588        Ok(principal)
589    }
590
591    fn hash_key(key: &str) -> String {
592        let mut hasher = Sha256::new();
593        hasher.update(key.as_bytes());
594        let result = hasher.finalize();
595        BASE64.encode(result)
596    }
597}
598
599#[cfg(test)]
600mod tests {
601    use super::*;
602    use jsonwebtoken::{EncodingKey, Header, encode};
603    use std::env;
604    use std::io::Write;
605
606    /// Helper: create a JwtSettings for HMAC-based algorithms
607    fn hmac_jwt_settings(algorithm: &str, secret: &str) -> JwtSettings {
608        JwtSettings {
609            enabled: true,
610            secret: Some(secret.to_string()),
611            public_key_path: None,
612            ec_public_key_path: None,
613            ed_public_key_path: None,
614            algorithm: algorithm.to_string(),
615            expiration_secs: 3600,
616            issuer: None,
617            audience: None,
618        }
619    }
620
621    /// Helper: create JWT claims with expiration in the future
622    fn make_claims(sub: &str) -> JwtClaims {
623        let now = std::time::SystemTime::now()
624            .duration_since(std::time::UNIX_EPOCH)
625            .expect("system time before epoch")
626            .as_secs() as usize;
627        JwtClaims {
628            sub: sub.to_string(),
629            exp: now + 3600,
630            iat: Some(now),
631            iss: None,
632            aud: None,
633            name: Some(format!("User {}", sub)),
634            roles: Some(vec!["admin".to_string()]),
635            attributes: HashMap::new(),
636        }
637    }
638
639    /// Helper: create expired JWT claims
640    fn make_expired_claims(sub: &str) -> JwtClaims {
641        let now = std::time::SystemTime::now()
642            .duration_since(std::time::UNIX_EPOCH)
643            .expect("system time before epoch")
644            .as_secs() as usize;
645        JwtClaims {
646            sub: sub.to_string(),
647            exp: now.saturating_sub(3600), // expired 1 hour ago
648            iat: Some(now.saturating_sub(7200)),
649            iss: None,
650            aud: None,
651            name: Some(format!("User {}", sub)),
652            roles: None,
653            attributes: HashMap::new(),
654        }
655    }
656
657    /// Helper: write content to a temporary file and return the path
658    fn write_temp_key(name: &str, content: &[u8]) -> std::path::PathBuf {
659        let dir = std::env::temp_dir().join("amaters_jwt_test");
660        std::fs::create_dir_all(&dir).expect("failed to create temp dir");
661        let path = dir.join(name);
662        let mut file = std::fs::File::create(&path).expect("failed to create temp file");
663        file.write_all(content).expect("failed to write temp file");
664        path
665    }
666
667    /// Generate RSA key pair PEM bytes (private, public) using openssl-like approach
668    /// We use the jsonwebtoken crate's own key parsing to ensure compatibility.
669    /// These are pre-generated 2048-bit RSA test keys.
670    fn rsa_test_keys() -> (&'static [u8], &'static [u8]) {
671        // Pre-generated 2048-bit RSA key pair for testing
672        (
673            include_bytes!("../tests/fixtures/rsa_private.pem"),
674            include_bytes!("../tests/fixtures/rsa_public.pem"),
675        )
676    }
677
678    fn ec256_test_keys() -> (&'static [u8], &'static [u8]) {
679        (
680            include_bytes!("../tests/fixtures/ec256_private.pem"),
681            include_bytes!("../tests/fixtures/ec256_public.pem"),
682        )
683    }
684
685    fn ec384_test_keys() -> (&'static [u8], &'static [u8]) {
686        (
687            include_bytes!("../tests/fixtures/ec384_private.pem"),
688            include_bytes!("../tests/fixtures/ec384_public.pem"),
689        )
690    }
691
692    fn ed25519_test_keys() -> (&'static [u8], &'static [u8]) {
693        (
694            include_bytes!("../tests/fixtures/ed25519_private.pem"),
695            include_bytes!("../tests/fixtures/ed25519_public.pem"),
696        )
697    }
698
699    #[test]
700    fn test_principal_creation() {
701        let principal = Principal::new(
702            "user123".to_string(),
703            "John Doe".to_string(),
704            AuthMethod::Jwt,
705        );
706
707        assert_eq!(principal.id, "user123");
708        assert_eq!(principal.name, "John Doe");
709        assert_eq!(principal.auth_method, AuthMethod::Jwt);
710    }
711
712    #[test]
713    fn test_principal_attributes() {
714        let principal = Principal::new(
715            "user123".to_string(),
716            "John Doe".to_string(),
717            AuthMethod::Jwt,
718        )
719        .with_attribute("role".to_string(), "admin".to_string())
720        .with_attribute("department".to_string(), "Engineering".to_string());
721
722        assert_eq!(principal.get_attribute("role"), Some(&"admin".to_string()));
723        assert!(principal.has_role("admin"));
724        assert!(!principal.has_role("user"));
725    }
726
727    #[test]
728    fn test_api_key_hashing() {
729        let key = "test-api-key-12345";
730        let hash1 = ApiKeyValidator::hash_key(key);
731        let hash2 = ApiKeyValidator::hash_key(key);
732
733        assert_eq!(hash1, hash2); // Same key produces same hash
734        assert!(!hash1.is_empty());
735    }
736
737    #[test]
738    fn test_authenticator_creation() {
739        let config = AuthSettings {
740            enabled: true,
741            methods: vec!["mtls".to_string()],
742            mtls: MtlsSettings {
743                enabled: true,
744                ca_certs_dir: Some(env::temp_dir()),
745                crl_path: None,
746                verify_cn: true,
747                allowed_organizations: Vec::new(),
748            },
749            jwt: JwtSettings::default(),
750            api_key: ApiKeySettings::default(),
751            reject_unauthenticated: true,
752        };
753
754        let auth_result = Authenticator::new(config);
755        assert!(auth_result.is_ok());
756    }
757
758    #[test]
759    fn test_jwt_validator_creation_missing_secret() {
760        let config = JwtSettings {
761            enabled: true,
762            secret: None,
763            public_key_path: None,
764            ec_public_key_path: None,
765            ed_public_key_path: None,
766            algorithm: "HS256".to_string(),
767            expiration_secs: 3600,
768            issuer: None,
769            audience: None,
770        };
771
772        let result = JwtValidator::new(config);
773        assert!(result.is_err());
774    }
775
776    #[test]
777    fn test_auth_method_display() {
778        assert_eq!(format!("{}", AuthMethod::MutualTls), "mTLS");
779        assert_eq!(format!("{}", AuthMethod::Jwt), "JWT");
780        assert_eq!(format!("{}", AuthMethod::ApiKey), "API Key");
781    }
782
783    // ---- JWT algorithm validation tests ----
784
785    #[test]
786    fn test_jwt_hs256_validation() {
787        let secret = "super-secret-key-for-hs256-testing";
788        let config = hmac_jwt_settings("HS256", secret);
789        let validator = JwtValidator::new(config).expect("HS256 validator creation failed");
790
791        let claims = make_claims("user-hs256");
792        let header = Header::new(Algorithm::HS256);
793        let encoding_key = EncodingKey::from_secret(secret.as_bytes());
794        let token = encode(&header, &claims, &encoding_key).expect("HS256 token encoding failed");
795
796        let principal = validator
797            .validate_token(&token)
798            .expect("HS256 token validation failed");
799        assert_eq!(principal.id, "user-hs256");
800        assert_eq!(principal.auth_method, AuthMethod::Jwt);
801    }
802
803    #[test]
804    fn test_jwt_rs256_validation() {
805        let (private_pem, public_pem) = rsa_test_keys();
806        let pub_path = write_temp_key("rs256_pub.pem", public_pem);
807
808        let config = JwtSettings {
809            enabled: true,
810            secret: None,
811            public_key_path: Some(pub_path),
812            ec_public_key_path: None,
813            ed_public_key_path: None,
814            algorithm: "RS256".to_string(),
815            expiration_secs: 3600,
816            issuer: None,
817            audience: None,
818        };
819        let validator = JwtValidator::new(config).expect("RS256 validator creation failed");
820
821        let claims = make_claims("user-rs256");
822        let header = Header::new(Algorithm::RS256);
823        let encoding_key =
824            EncodingKey::from_rsa_pem(private_pem).expect("RS256 encoding key creation failed");
825        let token = encode(&header, &claims, &encoding_key).expect("RS256 token encoding failed");
826
827        let principal = validator
828            .validate_token(&token)
829            .expect("RS256 token validation failed");
830        assert_eq!(principal.id, "user-rs256");
831    }
832
833    #[test]
834    fn test_jwt_rs384_validation() {
835        let (private_pem, public_pem) = rsa_test_keys();
836        let pub_path = write_temp_key("rs384_pub.pem", public_pem);
837
838        let config = JwtSettings {
839            enabled: true,
840            secret: None,
841            public_key_path: Some(pub_path),
842            ec_public_key_path: None,
843            ed_public_key_path: None,
844            algorithm: "RS384".to_string(),
845            expiration_secs: 3600,
846            issuer: None,
847            audience: None,
848        };
849        let validator = JwtValidator::new(config).expect("RS384 validator creation failed");
850
851        let claims = make_claims("user-rs384");
852        let header = Header::new(Algorithm::RS384);
853        let encoding_key =
854            EncodingKey::from_rsa_pem(private_pem).expect("RS384 encoding key creation failed");
855        let token = encode(&header, &claims, &encoding_key).expect("RS384 token encoding failed");
856
857        let principal = validator
858            .validate_token(&token)
859            .expect("RS384 token validation failed");
860        assert_eq!(principal.id, "user-rs384");
861    }
862
863    #[test]
864    fn test_jwt_rs512_validation() {
865        let (private_pem, public_pem) = rsa_test_keys();
866        let pub_path = write_temp_key("rs512_pub.pem", public_pem);
867
868        let config = JwtSettings {
869            enabled: true,
870            secret: None,
871            public_key_path: Some(pub_path),
872            ec_public_key_path: None,
873            ed_public_key_path: None,
874            algorithm: "RS512".to_string(),
875            expiration_secs: 3600,
876            issuer: None,
877            audience: None,
878        };
879        let validator = JwtValidator::new(config).expect("RS512 validator creation failed");
880
881        let claims = make_claims("user-rs512");
882        let header = Header::new(Algorithm::RS512);
883        let encoding_key =
884            EncodingKey::from_rsa_pem(private_pem).expect("RS512 encoding key creation failed");
885        let token = encode(&header, &claims, &encoding_key).expect("RS512 token encoding failed");
886
887        let principal = validator
888            .validate_token(&token)
889            .expect("RS512 token validation failed");
890        assert_eq!(principal.id, "user-rs512");
891    }
892
893    #[test]
894    fn test_jwt_es256_validation() {
895        let (private_pem, public_pem) = ec256_test_keys();
896        let pub_path = write_temp_key("es256_pub.pem", public_pem);
897
898        let config = JwtSettings {
899            enabled: true,
900            secret: None,
901            public_key_path: None,
902            ec_public_key_path: Some(pub_path),
903            ed_public_key_path: None,
904            algorithm: "ES256".to_string(),
905            expiration_secs: 3600,
906            issuer: None,
907            audience: None,
908        };
909        let validator = JwtValidator::new(config).expect("ES256 validator creation failed");
910
911        let claims = make_claims("user-es256");
912        let header = Header::new(Algorithm::ES256);
913        let encoding_key =
914            EncodingKey::from_ec_pem(private_pem).expect("ES256 encoding key creation failed");
915        let token = encode(&header, &claims, &encoding_key).expect("ES256 token encoding failed");
916
917        let principal = validator
918            .validate_token(&token)
919            .expect("ES256 token validation failed");
920        assert_eq!(principal.id, "user-es256");
921    }
922
923    #[test]
924    fn test_jwt_es384_validation() {
925        let (private_pem, public_pem) = ec384_test_keys();
926        let pub_path = write_temp_key("es384_pub.pem", public_pem);
927
928        let config = JwtSettings {
929            enabled: true,
930            secret: None,
931            public_key_path: None,
932            ec_public_key_path: Some(pub_path),
933            ed_public_key_path: None,
934            algorithm: "ES384".to_string(),
935            expiration_secs: 3600,
936            issuer: None,
937            audience: None,
938        };
939        let validator = JwtValidator::new(config).expect("ES384 validator creation failed");
940
941        let claims = make_claims("user-es384");
942        let header = Header::new(Algorithm::ES384);
943        let encoding_key =
944            EncodingKey::from_ec_pem(private_pem).expect("ES384 encoding key creation failed");
945        let token = encode(&header, &claims, &encoding_key).expect("ES384 token encoding failed");
946
947        let principal = validator
948            .validate_token(&token)
949            .expect("ES384 token validation failed");
950        assert_eq!(principal.id, "user-es384");
951    }
952
953    #[test]
954    fn test_jwt_eddsa_validation() {
955        let (private_pem, public_pem) = ed25519_test_keys();
956        let pub_path = write_temp_key("eddsa_pub.pem", public_pem);
957
958        let config = JwtSettings {
959            enabled: true,
960            secret: None,
961            public_key_path: None,
962            ec_public_key_path: None,
963            ed_public_key_path: Some(pub_path),
964            algorithm: "EdDSA".to_string(),
965            expiration_secs: 3600,
966            issuer: None,
967            audience: None,
968        };
969        let validator = JwtValidator::new(config).expect("EdDSA validator creation failed");
970
971        let claims = make_claims("user-eddsa");
972        let header = Header::new(Algorithm::EdDSA);
973        let encoding_key =
974            EncodingKey::from_ed_pem(private_pem).expect("EdDSA encoding key creation failed");
975        let token = encode(&header, &claims, &encoding_key).expect("EdDSA token encoding failed");
976
977        let principal = validator
978            .validate_token(&token)
979            .expect("EdDSA token validation failed");
980        assert_eq!(principal.id, "user-eddsa");
981    }
982
983    #[test]
984    fn test_jwt_algorithm_mismatch() {
985        // Create an RS256 token but try to validate with HS256
986        let secret = "test-mismatch-secret";
987        let config = hmac_jwt_settings("HS256", secret);
988        let validator = JwtValidator::new(config).expect("HS256 validator creation failed");
989
990        let (private_pem, _) = rsa_test_keys();
991        let claims = make_claims("user-mismatch");
992        let header = Header::new(Algorithm::RS256);
993        let encoding_key =
994            EncodingKey::from_rsa_pem(private_pem).expect("RS256 encoding key creation failed");
995        let token = encode(&header, &claims, &encoding_key).expect("RS256 token encoding failed");
996
997        let result = validator.validate_token(&token);
998        assert!(result.is_err(), "Algorithm mismatch should fail validation");
999    }
1000
1001    #[test]
1002    fn test_jwt_missing_ec_key_path() {
1003        let config = JwtSettings {
1004            enabled: true,
1005            secret: None,
1006            public_key_path: None,
1007            ec_public_key_path: None, // Not configured
1008            ed_public_key_path: None,
1009            algorithm: "ES256".to_string(),
1010            expiration_secs: 3600,
1011            issuer: None,
1012            audience: None,
1013        };
1014
1015        let result = JwtValidator::new(config);
1016        assert!(result.is_err());
1017        let err_msg = format!("{}", result.expect_err("should be an error"));
1018        assert!(
1019            err_msg.contains("EC public key path not configured"),
1020            "Expected EC key path error, got: {}",
1021            err_msg
1022        );
1023    }
1024
1025    #[test]
1026    fn test_jwt_missing_ed_key_path() {
1027        let config = JwtSettings {
1028            enabled: true,
1029            secret: None,
1030            public_key_path: None,
1031            ec_public_key_path: None,
1032            ed_public_key_path: None, // Not configured
1033            algorithm: "EdDSA".to_string(),
1034            expiration_secs: 3600,
1035            issuer: None,
1036            audience: None,
1037        };
1038
1039        let result = JwtValidator::new(config);
1040        assert!(result.is_err());
1041        let err_msg = format!("{}", result.expect_err("should be an error"));
1042        assert!(
1043            err_msg.contains("Ed25519 public key path not configured"),
1044            "Expected Ed25519 key path error, got: {}",
1045            err_msg
1046        );
1047    }
1048
1049    #[test]
1050    fn test_jwt_invalid_ec_key() {
1051        // Write corrupt PEM content
1052        let corrupt_path = write_temp_key("corrupt_ec.pem", b"NOT A VALID PEM KEY");
1053
1054        let config = JwtSettings {
1055            enabled: true,
1056            secret: None,
1057            public_key_path: None,
1058            ec_public_key_path: Some(corrupt_path),
1059            ed_public_key_path: None,
1060            algorithm: "ES256".to_string(),
1061            expiration_secs: 3600,
1062            issuer: None,
1063            audience: None,
1064        };
1065
1066        let result = JwtValidator::new(config);
1067        assert!(result.is_err(), "Corrupt EC key should fail");
1068        let err_msg = format!("{}", result.expect_err("should be an error"));
1069        assert!(
1070            err_msg.contains("Failed to load EC public key"),
1071            "Expected EC key load error, got: {}",
1072            err_msg
1073        );
1074    }
1075
1076    #[test]
1077    fn test_jwt_expired_token() {
1078        let secret = "expiration-test-secret";
1079        let config = hmac_jwt_settings("HS256", secret);
1080        let validator = JwtValidator::new(config).expect("HS256 validator creation failed");
1081
1082        let claims = make_expired_claims("user-expired");
1083        let header = Header::new(Algorithm::HS256);
1084        let encoding_key = EncodingKey::from_secret(secret.as_bytes());
1085        let token = encode(&header, &claims, &encoding_key).expect("Expired token encoding failed");
1086
1087        let result = validator.validate_token(&token);
1088        assert!(result.is_err(), "Expired token should fail validation");
1089        let err_msg = format!("{}", result.expect_err("should be an error"));
1090        assert!(
1091            err_msg.contains("Token validation failed"),
1092            "Expected token validation error, got: {}",
1093            err_msg
1094        );
1095    }
1096
1097    #[test]
1098    fn test_jwt_unsupported_algorithm() {
1099        let config = JwtSettings {
1100            enabled: true,
1101            secret: Some("secret".to_string()),
1102            public_key_path: None,
1103            ec_public_key_path: None,
1104            ed_public_key_path: None,
1105            algorithm: "UNSUPPORTED".to_string(),
1106            expiration_secs: 3600,
1107            issuer: None,
1108            audience: None,
1109        };
1110
1111        let result = JwtValidator::new(config);
1112        assert!(result.is_err());
1113        let err_msg = format!("{}", result.expect_err("should be an error"));
1114        assert!(
1115            err_msg.contains("Unsupported JWT algorithm"),
1116            "Expected unsupported algorithm error, got: {}",
1117            err_msg
1118        );
1119    }
1120}