auth_framework/server/jwt/
jwt_best_practices.rs

1//! RFC 8725 - JSON Web Token Best Current Practices
2//!
3//! This module implements security best practices for JSON Web Tokens (JWTs)
4//! as defined in RFC 8725, providing enhanced security validation and
5//! configuration guidelines.
6
7use crate::errors::{AuthError, Result};
8use chrono::Utc;
9use jsonwebtoken::{Algorithm, Validation};
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use std::collections::{HashMap, HashSet};
13
14/// JWT Security Level according to RFC 8725
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum SecurityLevel {
17    /// Minimum security requirements
18    Minimum,
19    /// Recommended security practices
20    Recommended,
21    /// Maximum security (defense in depth)
22    Maximum,
23}
24
25/// Cryptographic strength classification
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
27pub enum CryptoStrength {
28    /// Weak algorithms (not recommended)
29    Weak,
30    /// Acceptable algorithms
31    Acceptable,
32    /// Strong algorithms (recommended)
33    Strong,
34    /// High-strength algorithms (maximum security)
35    High,
36}
37
38/// JWT Best Practices Configuration
39#[derive(Debug, Clone)]
40pub struct JwtBestPracticesConfig {
41    /// Required security level
42    pub security_level: SecurityLevel,
43
44    /// Allowed signing algorithms (in order of preference)
45    pub allowed_algorithms: Vec<Algorithm>,
46
47    /// Forbidden algorithms (explicitly denied)
48    pub forbidden_algorithms: Vec<Algorithm>,
49
50    /// Maximum token lifetime (seconds)
51    pub max_lifetime: i64,
52
53    /// Minimum token lifetime (seconds)
54    pub min_lifetime: i64,
55
56    /// Clock skew tolerance (seconds)
57    pub clock_skew: i64,
58
59    /// Required issuer(s)
60    pub required_issuers: HashSet<String>,
61
62    /// Required audience(s)
63    pub required_audiences: HashSet<String>,
64
65    /// Whether to require the 'sub' claim
66    pub require_subject: bool,
67
68    /// Whether to require the 'iat' claim
69    pub require_issued_at: bool,
70
71    /// Whether to require the 'exp' claim
72    pub require_expiration: bool,
73
74    /// Whether to require the 'nbf' claim
75    pub require_not_before: bool,
76
77    /// Whether to require the 'jti' claim (replay protection)
78    pub require_jwt_id: bool,
79
80    /// Maximum allowed nested JWT depth
81    pub max_nested_depth: u8,
82}
83
84impl Default for JwtBestPracticesConfig {
85    fn default() -> Self {
86        Self {
87            security_level: SecurityLevel::Recommended,
88            allowed_algorithms: vec![
89                Algorithm::RS256,
90                Algorithm::RS384,
91                Algorithm::RS512,
92                Algorithm::ES256,
93                Algorithm::ES384,
94                Algorithm::EdDSA,
95                Algorithm::PS256,
96                Algorithm::PS384,
97                Algorithm::PS512,
98                Algorithm::EdDSA,
99            ],
100            forbidden_algorithms: vec![],
101            max_lifetime: 3600, // 1 hour
102            min_lifetime: 60,   // 1 minute
103            clock_skew: 30,     // 30 seconds
104            required_issuers: HashSet::new(),
105            required_audiences: HashSet::new(),
106            require_subject: true,
107            require_issued_at: true,
108            require_expiration: true,
109            require_not_before: false,
110            require_jwt_id: false,
111            max_nested_depth: 1,
112        }
113    }
114}
115
116impl JwtBestPracticesConfig {
117    /// Create configuration for minimum security level
118    pub fn minimum_security() -> Self {
119        Self {
120            security_level: SecurityLevel::Minimum,
121            allowed_algorithms: vec![Algorithm::RS256, Algorithm::ES256, Algorithm::PS256],
122            max_lifetime: 86400, // 24 hours
123            require_subject: false,
124            require_issued_at: false,
125            require_jwt_id: false,
126            ..Default::default()
127        }
128    }
129
130    /// Create configuration for maximum security level
131    pub fn maximum_security() -> Self {
132        Self {
133            security_level: SecurityLevel::Maximum,
134            allowed_algorithms: vec![
135                Algorithm::ES384,
136                Algorithm::EdDSA,
137                Algorithm::PS384,
138                Algorithm::PS512,
139                Algorithm::EdDSA,
140            ],
141            forbidden_algorithms: vec![Algorithm::HS256, Algorithm::HS384, Algorithm::HS512],
142            max_lifetime: 900, // 15 minutes
143            min_lifetime: 30,  // 30 seconds
144            clock_skew: 5,     // 5 seconds
145            require_subject: true,
146            require_issued_at: true,
147            require_expiration: true,
148            require_not_before: true,
149            require_jwt_id: true,
150            max_nested_depth: 0, // No nesting
151            ..Default::default()
152        }
153    }
154}
155
156/// Algorithm security classification functions
157pub fn get_algorithm_crypto_strength(algorithm: &Algorithm) -> CryptoStrength {
158    match algorithm {
159        Algorithm::HS256 => CryptoStrength::Acceptable,
160        Algorithm::HS384 => CryptoStrength::Strong,
161        Algorithm::HS512 => CryptoStrength::Strong,
162        Algorithm::RS256 => CryptoStrength::Acceptable,
163        Algorithm::RS384 => CryptoStrength::Strong,
164        Algorithm::RS512 => CryptoStrength::Strong,
165        Algorithm::ES256 => CryptoStrength::Strong,
166        Algorithm::ES384 => CryptoStrength::High,
167        Algorithm::EdDSA => CryptoStrength::High,
168        Algorithm::PS256 => CryptoStrength::Strong,
169        Algorithm::PS384 => CryptoStrength::High,
170        Algorithm::PS512 => CryptoStrength::High,
171    }
172}
173
174pub fn is_algorithm_symmetric(algorithm: &Algorithm) -> bool {
175    matches!(
176        algorithm,
177        Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512
178    )
179}
180
181pub fn is_algorithm_asymmetric(algorithm: &Algorithm) -> bool {
182    !is_algorithm_symmetric(algorithm)
183}
184
185/// Standard JWT claims with validation
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct SecureJwtClaims {
188    /// Issuer
189    pub iss: String,
190
191    /// Subject
192    pub sub: String,
193
194    /// Audience
195    pub aud: Vec<String>,
196
197    /// Expiration time
198    pub exp: i64,
199
200    /// Not before
201    pub nbf: Option<i64>,
202
203    /// Issued at
204    pub iat: i64,
205
206    /// JWT ID
207    pub jti: String,
208
209    /// Custom claims
210    #[serde(flatten)]
211    pub custom: HashMap<String, Value>,
212}
213
214/// JWT Best Practices Validator
215pub struct JwtBestPracticesValidator {
216    config: JwtBestPracticesConfig,
217    used_jtis: HashSet<String>, // Simple replay protection
218}
219
220impl JwtBestPracticesValidator {
221    /// Create a new validator with configuration
222    pub fn new(config: JwtBestPracticesConfig) -> Self {
223        Self {
224            config,
225            used_jtis: HashSet::new(),
226        }
227    }
228
229    /// Validate JWT token format
230    pub fn validate_token_format(&self, token: &str) -> Result<()> {
231        let parts: Vec<&str> = token.split('.').collect();
232        if parts.len() != 3 {
233            return Err(AuthError::InvalidToken("Invalid JWT format".to_string()));
234        }
235
236        // Check for excessive size (potential DoS)
237        if token.len() > 8192 {
238            return Err(AuthError::InvalidToken("Token too large".to_string()));
239        }
240
241        Ok(())
242    }
243
244    /// Validate algorithm security
245    pub fn validate_algorithm(&self, algorithm: &Algorithm) -> Result<()> {
246        // Check if algorithm is forbidden
247        if self.config.forbidden_algorithms.contains(algorithm) {
248            return Err(AuthError::InvalidToken(format!(
249                "Forbidden algorithm: {:?}",
250                algorithm
251            )));
252        }
253
254        // Check if algorithm is allowed
255        if !self.config.allowed_algorithms.contains(algorithm) {
256            return Err(AuthError::InvalidToken(format!(
257                "Algorithm not allowed: {:?}",
258                algorithm
259            )));
260        }
261
262        // Check crypto strength
263        let strength = get_algorithm_crypto_strength(algorithm);
264        match self.config.security_level {
265            SecurityLevel::Minimum => {
266                if strength < CryptoStrength::Acceptable {
267                    return Err(AuthError::InvalidToken("Algorithm too weak".to_string()));
268                }
269            }
270            SecurityLevel::Recommended => {
271                if strength < CryptoStrength::Strong {
272                    return Err(AuthError::InvalidToken(
273                        "Algorithm not recommended".to_string(),
274                    ));
275                }
276            }
277            SecurityLevel::Maximum => {
278                if strength < CryptoStrength::High {
279                    return Err(AuthError::InvalidToken(
280                        "Algorithm insufficient for maximum security".to_string(),
281                    ));
282                }
283            }
284        }
285
286        Ok(())
287    }
288
289    /// Validate standard JWT claims
290    pub fn validate_standard_claims(&mut self, claims: &SecureJwtClaims) -> Result<()> {
291        let now = Utc::now().timestamp();
292
293        // Validate expiration
294        if claims.exp <= now {
295            return Err(AuthError::InvalidToken("Token has expired".to_string()));
296        }
297
298        // Validate not before
299        if let Some(nbf) = claims.nbf
300            && nbf > now + self.config.clock_skew
301        {
302            return Err(AuthError::InvalidToken(
303                "Token is not yet valid".to_string(),
304            ));
305        }
306
307        // Validate issued at
308        if claims.iat > now + self.config.clock_skew {
309            return Err(AuthError::InvalidToken(
310                "Token issued in the future".to_string(),
311            ));
312        }
313
314        // Validate lifetime
315        let lifetime = claims.exp - claims.iat;
316        if lifetime > self.config.max_lifetime {
317            return Err(AuthError::InvalidToken(
318                "Token lifetime too long".to_string(),
319            ));
320        }
321        if lifetime < self.config.min_lifetime {
322            return Err(AuthError::InvalidToken(
323                "Token lifetime too short".to_string(),
324            ));
325        }
326
327        // Validate issuer
328        if !self.config.required_issuers.is_empty()
329            && !self.config.required_issuers.contains(&claims.iss)
330        {
331            return Err(AuthError::InvalidToken("Invalid issuer".to_string()));
332        }
333
334        // Validate audience
335        if !self.config.required_audiences.is_empty() {
336            let has_valid_audience = claims
337                .aud
338                .iter()
339                .any(|aud| self.config.required_audiences.contains(aud));
340            if !has_valid_audience {
341                return Err(AuthError::InvalidToken("Invalid audience".to_string()));
342            }
343        }
344
345        // Validate JWT ID for replay protection
346        if self.config.require_jwt_id {
347            if self.used_jtis.contains(&claims.jti) {
348                return Err(AuthError::InvalidToken("Token replay detected".to_string()));
349            }
350            self.used_jtis.insert(claims.jti.clone());
351        }
352
353        Ok(())
354    }
355
356    /// Create validation rules based on configuration
357    pub fn create_validation_rules(&self, algorithm: Algorithm) -> Result<Validation> {
358        let mut validation = Validation::new(algorithm);
359
360        // Configure time-based validation
361        validation.leeway = self.config.clock_skew as u64;
362        validation.validate_exp = self.config.require_expiration;
363        validation.validate_nbf = self.config.require_not_before;
364
365        // Configure issuer validation
366        if !self.config.required_issuers.is_empty() {
367            let issuers: Vec<&str> = self
368                .config
369                .required_issuers
370                .iter()
371                .map(|s| s.as_str())
372                .collect();
373            validation.set_issuer(&issuers);
374        }
375
376        // Configure audience validation
377        if !self.config.required_audiences.is_empty() {
378            let audiences: Vec<&str> = self
379                .config
380                .required_audiences
381                .iter()
382                .map(|s| s.as_str())
383                .collect();
384            validation.set_audience(&audiences);
385        }
386
387        Ok(validation)
388    }
389
390    /// Get security recommendations for current configuration
391    pub fn get_security_recommendations(&self) -> Vec<String> {
392        let mut recommendations = Vec::new();
393
394        // Algorithm recommendations
395        if self
396            .config
397            .allowed_algorithms
398            .iter()
399            .any(is_algorithm_symmetric)
400        {
401            recommendations.push(
402                "Consider using asymmetric algorithms (RS*, ES*, PS*) for better security"
403                    .to_string(),
404            );
405        }
406
407        // Lifetime recommendations
408        if self.config.max_lifetime > 3600 {
409            recommendations.push("Consider reducing token lifetime to 1 hour or less".to_string());
410        }
411
412        // Claims recommendations
413        if !self.config.require_jwt_id {
414            recommendations
415                .push("Consider enabling JWT ID (jti) claim for replay protection".to_string());
416        }
417
418        if !self.config.require_issued_at {
419            recommendations
420                .push("Consider requiring issued at (iat) claim for better validation".to_string());
421        }
422
423        recommendations
424    }
425
426    /// Clear used JWT IDs (for cleanup)
427    pub fn clear_used_jtis(&mut self) {
428        self.used_jtis.clear();
429    }
430
431    /// Get current configuration
432    pub fn get_config(&self) -> &JwtBestPracticesConfig {
433        &self.config
434    }
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440
441    #[test]
442    fn test_algorithm_strength_classification() {
443        assert_eq!(
444            get_algorithm_crypto_strength(&Algorithm::HS256),
445            CryptoStrength::Acceptable
446        );
447        assert_eq!(
448            get_algorithm_crypto_strength(&Algorithm::ES384),
449            CryptoStrength::High
450        );
451        assert_eq!(
452            get_algorithm_crypto_strength(&Algorithm::EdDSA),
453            CryptoStrength::High
454        );
455    }
456
457    #[test]
458    fn test_security_level_configuration() {
459        let min_config = JwtBestPracticesConfig::minimum_security();
460        let max_config = JwtBestPracticesConfig::maximum_security();
461
462        assert_eq!(min_config.security_level, SecurityLevel::Minimum);
463        assert_eq!(max_config.security_level, SecurityLevel::Maximum);
464        assert!(max_config.max_lifetime < min_config.max_lifetime);
465        assert!(max_config.require_jwt_id);
466        assert!(!min_config.require_jwt_id);
467    }
468
469    #[test]
470    fn test_jwt_best_practices_validation() {
471        let config = JwtBestPracticesConfig::default();
472        let validator = JwtBestPracticesValidator::new(config);
473
474        // Test algorithm validation
475        assert!(validator.validate_algorithm(&Algorithm::ES256).is_ok());
476    }
477
478    #[test]
479    fn test_token_format_validation() {
480        let config = JwtBestPracticesConfig::default();
481        let validator = JwtBestPracticesValidator::new(config);
482
483        assert!(
484            validator
485                .validate_token_format("header.payload.signature")
486                .is_ok()
487        );
488        assert!(validator.validate_token_format("invalid.format").is_err());
489        assert!(
490            validator
491                .validate_token_format("too.many.parts.here")
492                .is_err()
493        );
494    }
495}
496
497