Skip to main content

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::token("Invalid JWT format".to_string()));
234        }
235
236        // Check for excessive size (potential DoS)
237        if token.len() > 8192 {
238            return Err(AuthError::token("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::token(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::token(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::token("Algorithm too weak".to_string()));
268                }
269            }
270            SecurityLevel::Recommended => {
271                if strength < CryptoStrength::Strong {
272                    return Err(AuthError::token("Algorithm not recommended".to_string()));
273                }
274            }
275            SecurityLevel::Maximum => {
276                if strength < CryptoStrength::High {
277                    return Err(AuthError::token(
278                        "Algorithm insufficient for maximum security".to_string(),
279                    ));
280                }
281            }
282        }
283
284        Ok(())
285    }
286
287    /// Validate standard JWT claims
288    pub fn validate_standard_claims(&mut self, claims: &SecureJwtClaims) -> Result<()> {
289        let now = Utc::now().timestamp();
290
291        // Validate expiration
292        if claims.exp <= now {
293            return Err(AuthError::token("Token has expired".to_string()));
294        }
295
296        // Validate not before
297        if let Some(nbf) = claims.nbf
298            && nbf > now + self.config.clock_skew
299        {
300            return Err(AuthError::token("Token is not yet valid".to_string()));
301        }
302
303        // Validate issued at
304        if claims.iat > now + self.config.clock_skew {
305            return Err(AuthError::token("Token issued in the future".to_string()));
306        }
307
308        // Validate lifetime
309        let lifetime = claims.exp - claims.iat;
310        if lifetime > self.config.max_lifetime {
311            return Err(AuthError::token("Token lifetime too long".to_string()));
312        }
313        if lifetime < self.config.min_lifetime {
314            return Err(AuthError::token("Token lifetime too short".to_string()));
315        }
316
317        // Validate issuer
318        if !self.config.required_issuers.is_empty()
319            && !self.config.required_issuers.contains(&claims.iss)
320        {
321            return Err(AuthError::token("Invalid issuer".to_string()));
322        }
323
324        // Validate audience
325        if !self.config.required_audiences.is_empty() {
326            let has_valid_audience = claims
327                .aud
328                .iter()
329                .any(|aud| self.config.required_audiences.contains(aud));
330            if !has_valid_audience {
331                return Err(AuthError::token("Invalid audience".to_string()));
332            }
333        }
334
335        // Validate JWT ID for replay protection
336        if self.config.require_jwt_id {
337            if self.used_jtis.contains(&claims.jti) {
338                return Err(AuthError::token("Token replay detected".to_string()));
339            }
340            self.used_jtis.insert(claims.jti.clone());
341        }
342
343        Ok(())
344    }
345
346    /// Create validation rules based on configuration
347    pub fn create_validation_rules(&self, algorithm: Algorithm) -> Result<Validation> {
348        let mut validation = Validation::new(algorithm);
349
350        // Configure time-based validation
351        validation.leeway = self.config.clock_skew as u64;
352        validation.validate_exp = self.config.require_expiration;
353        validation.validate_nbf = self.config.require_not_before;
354
355        // Configure issuer validation
356        if !self.config.required_issuers.is_empty() {
357            let issuers: Vec<&str> = self
358                .config
359                .required_issuers
360                .iter()
361                .map(|s| s.as_str())
362                .collect();
363            validation.set_issuer(&issuers);
364        }
365
366        // Configure audience validation
367        if !self.config.required_audiences.is_empty() {
368            let audiences: Vec<&str> = self
369                .config
370                .required_audiences
371                .iter()
372                .map(|s| s.as_str())
373                .collect();
374            validation.set_audience(&audiences);
375        }
376
377        Ok(validation)
378    }
379
380    /// Get security recommendations for current configuration
381    pub fn get_security_recommendations(&self) -> Vec<String> {
382        let mut recommendations = Vec::new();
383
384        // Algorithm recommendations
385        if self
386            .config
387            .allowed_algorithms
388            .iter()
389            .any(is_algorithm_symmetric)
390        {
391            recommendations.push(
392                "Consider using asymmetric algorithms (RS*, ES*, PS*) for better security"
393                    .to_string(),
394            );
395        }
396
397        // Lifetime recommendations
398        if self.config.max_lifetime > 3600 {
399            recommendations.push("Consider reducing token lifetime to 1 hour or less".to_string());
400        }
401
402        // Claims recommendations
403        if !self.config.require_jwt_id {
404            recommendations
405                .push("Consider enabling JWT ID (jti) claim for replay protection".to_string());
406        }
407
408        if !self.config.require_issued_at {
409            recommendations
410                .push("Consider requiring issued at (iat) claim for better validation".to_string());
411        }
412
413        recommendations
414    }
415
416    /// Clear used JWT IDs (for cleanup)
417    pub fn clear_used_jtis(&mut self) {
418        self.used_jtis.clear();
419    }
420
421    /// Get current configuration
422    pub fn get_config(&self) -> &JwtBestPracticesConfig {
423        &self.config
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430
431    #[test]
432    fn test_algorithm_strength_classification() {
433        assert_eq!(
434            get_algorithm_crypto_strength(&Algorithm::HS256),
435            CryptoStrength::Acceptable
436        );
437        assert_eq!(
438            get_algorithm_crypto_strength(&Algorithm::ES384),
439            CryptoStrength::High
440        );
441        assert_eq!(
442            get_algorithm_crypto_strength(&Algorithm::EdDSA),
443            CryptoStrength::High
444        );
445    }
446
447    #[test]
448    fn test_security_level_configuration() {
449        let min_config = JwtBestPracticesConfig::minimum_security();
450        let max_config = JwtBestPracticesConfig::maximum_security();
451
452        assert_eq!(min_config.security_level, SecurityLevel::Minimum);
453        assert_eq!(max_config.security_level, SecurityLevel::Maximum);
454        assert!(max_config.max_lifetime < min_config.max_lifetime);
455        assert!(max_config.require_jwt_id);
456        assert!(!min_config.require_jwt_id);
457    }
458
459    #[test]
460    fn test_jwt_best_practices_validation() {
461        let config = JwtBestPracticesConfig::default();
462        let validator = JwtBestPracticesValidator::new(config);
463
464        // Test algorithm validation
465        assert!(validator.validate_algorithm(&Algorithm::ES256).is_ok());
466    }
467
468    #[test]
469    fn test_token_format_validation() {
470        let config = JwtBestPracticesConfig::default();
471        let validator = JwtBestPracticesValidator::new(config);
472
473        assert!(
474            validator
475                .validate_token_format("header.payload.signature")
476                .is_ok()
477        );
478        assert!(validator.validate_token_format("invalid.format").is_err());
479        assert!(
480            validator
481                .validate_token_format("too.many.parts.here")
482                .is_err()
483        );
484    }
485}