auth_framework/server/jwt/
jwt_best_practices.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum SecurityLevel {
17 Minimum,
19 Recommended,
21 Maximum,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
27pub enum CryptoStrength {
28 Weak,
30 Acceptable,
32 Strong,
34 High,
36}
37
38#[derive(Debug, Clone)]
40pub struct JwtBestPracticesConfig {
41 pub security_level: SecurityLevel,
43
44 pub allowed_algorithms: Vec<Algorithm>,
46
47 pub forbidden_algorithms: Vec<Algorithm>,
49
50 pub max_lifetime: i64,
52
53 pub min_lifetime: i64,
55
56 pub clock_skew: i64,
58
59 pub required_issuers: HashSet<String>,
61
62 pub required_audiences: HashSet<String>,
64
65 pub require_subject: bool,
67
68 pub require_issued_at: bool,
70
71 pub require_expiration: bool,
73
74 pub require_not_before: bool,
76
77 pub require_jwt_id: bool,
79
80 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, min_lifetime: 60, clock_skew: 30, 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 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, require_subject: false,
124 require_issued_at: false,
125 require_jwt_id: false,
126 ..Default::default()
127 }
128 }
129
130 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, min_lifetime: 30, clock_skew: 5, 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, ..Default::default()
152 }
153 }
154}
155
156pub 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#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct SecureJwtClaims {
188 pub iss: String,
190
191 pub sub: String,
193
194 pub aud: Vec<String>,
196
197 pub exp: i64,
199
200 pub nbf: Option<i64>,
202
203 pub iat: i64,
205
206 pub jti: String,
208
209 #[serde(flatten)]
211 pub custom: HashMap<String, Value>,
212}
213
214pub struct JwtBestPracticesValidator {
216 config: JwtBestPracticesConfig,
217 used_jtis: HashSet<String>, }
219
220impl JwtBestPracticesValidator {
221 pub fn new(config: JwtBestPracticesConfig) -> Self {
223 Self {
224 config,
225 used_jtis: HashSet::new(),
226 }
227 }
228
229 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 if token.len() > 8192 {
238 return Err(AuthError::token("Token too large".to_string()));
239 }
240
241 Ok(())
242 }
243
244 pub fn validate_algorithm(&self, algorithm: &Algorithm) -> Result<()> {
246 if self.config.forbidden_algorithms.contains(algorithm) {
248 return Err(AuthError::token(format!(
249 "Forbidden algorithm: {:?}",
250 algorithm
251 )));
252 }
253
254 if !self.config.allowed_algorithms.contains(algorithm) {
256 return Err(AuthError::token(format!(
257 "Algorithm not allowed: {:?}",
258 algorithm
259 )));
260 }
261
262 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 pub fn validate_standard_claims(&mut self, claims: &SecureJwtClaims) -> Result<()> {
289 let now = Utc::now().timestamp();
290
291 if claims.exp <= now {
293 return Err(AuthError::token("Token has expired".to_string()));
294 }
295
296 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 if claims.iat > now + self.config.clock_skew {
305 return Err(AuthError::token("Token issued in the future".to_string()));
306 }
307
308 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 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 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 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 pub fn create_validation_rules(&self, algorithm: Algorithm) -> Result<Validation> {
348 let mut validation = Validation::new(algorithm);
349
350 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 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 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 pub fn get_security_recommendations(&self) -> Vec<String> {
382 let mut recommendations = Vec::new();
383
384 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 if self.config.max_lifetime > 3600 {
399 recommendations.push("Consider reducing token lifetime to 1 hour or less".to_string());
400 }
401
402 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 pub fn clear_used_jtis(&mut self) {
418 self.used_jtis.clear();
419 }
420
421 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 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}