1use crate::errors::{AuthError, Result};
2use jsonwebtoken::{Algorithm, DecodingKey};
3use serde::{Deserialize, Serialize};
4use std::collections::HashSet;
5use std::time::Duration;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct SecureJwtClaims {
9 pub sub: String,
10 pub iss: String,
11 pub aud: String,
12 pub exp: i64,
13 pub nbf: i64,
14 pub iat: i64,
15 pub jti: String,
16 pub scope: String,
17 pub typ: String,
18 pub sid: Option<String>,
19 pub client_id: Option<String>,
20 pub auth_ctx_hash: Option<String>,
21}
22
23#[derive(Debug, Clone)]
24pub struct SecureJwtConfig {
25 pub allowed_algorithms: Vec<Algorithm>,
26 pub required_issuers: HashSet<String>,
27 pub required_audiences: HashSet<String>,
28 pub max_token_lifetime: Duration,
29 pub clock_skew: Duration,
30 pub require_jti: bool,
31 pub validate_nbf: bool,
32 pub allowed_token_types: HashSet<String>,
33 pub require_secure_transport: bool,
34 pub jwt_secret: String,
36 pub rsa_public_key_pem: Option<String>,
38 pub ec_public_key_pem: Option<String>,
40 pub ed_public_key_pem: Option<String>,
42}
43
44impl Default for SecureJwtConfig {
45 fn default() -> Self {
46 use ring::rand::{SecureRandom, SystemRandom};
53 let rng = SystemRandom::new();
56 let mut bytes = [0u8; 32];
57 rng.fill(&mut bytes)
58 .expect("AuthFramework fatal: system CSPRNG unavailable — the operating system cannot provide cryptographic randomness");
59 let jwt_secret = bytes.iter().fold(String::with_capacity(64), |mut s, b| {
60 s.push_str(&format!("{b:02x}"));
61 s
62 });
63
64 let mut allowed_token_types = HashSet::new();
65 allowed_token_types.insert("access".to_string());
66 allowed_token_types.insert("refresh".to_string());
67 allowed_token_types.insert("JARM".to_string());
68
69 let mut required_issuers = HashSet::new();
70 required_issuers.insert("auth-framework".to_string());
71
72 Self {
73 allowed_algorithms: vec![Algorithm::HS256],
77 required_issuers,
78 required_audiences: HashSet::new(),
79 max_token_lifetime: Duration::from_secs(3600),
80 clock_skew: Duration::from_secs(30),
81 require_jti: true,
82 validate_nbf: true,
83 allowed_token_types,
84 require_secure_transport: true,
85 jwt_secret,
86 rsa_public_key_pem: None,
87 ec_public_key_pem: None,
88 ed_public_key_pem: None,
89 }
90 }
91}
92
93fn is_hmac_algorithm(alg: Algorithm) -> bool {
95 matches!(alg, Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512)
96}
97
98impl SecureJwtConfig {
122 pub fn builder() -> SecureJwtConfigBuilder {
124 SecureJwtConfigBuilder::default()
125 }
126}
127
128pub struct SecureJwtConfigBuilder {
130 config: SecureJwtConfig,
131}
132
133impl Default for SecureJwtConfigBuilder {
134 fn default() -> Self {
135 Self {
136 config: SecureJwtConfig::default(),
137 }
138 }
139}
140
141impl SecureJwtConfigBuilder {
142 pub fn with_algorithm(mut self, algo: Algorithm) -> Self {
144 self.config.allowed_algorithms.push(algo);
145 self
146 }
147
148 pub fn with_algorithms(mut self, algos: Vec<Algorithm>) -> Self {
150 self.config.allowed_algorithms = algos;
151 self
152 }
153
154 pub fn require_issuer(mut self, issuer: impl Into<String>) -> Self {
156 self.config.required_issuers.insert(issuer.into());
157 self
158 }
159
160 pub fn require_audience(mut self, audience: impl Into<String>) -> Self {
162 self.config.required_audiences.insert(audience.into());
163 self
164 }
165
166 pub fn with_max_lifetime(mut self, lifetime: Duration) -> Self {
168 self.config.max_token_lifetime = lifetime;
169 self
170 }
171
172 pub fn with_clock_skew(mut self, skew: Duration) -> Self {
174 self.config.clock_skew = skew;
175 self
176 }
177
178 pub fn require_jti(mut self, require: bool) -> Self {
180 self.config.require_jti = require;
181 self
182 }
183
184 pub fn with_secret(mut self, secret: impl Into<String>) -> Self {
186 self.config.jwt_secret = secret.into();
187 self
188 }
189
190 pub fn build(self) -> SecureJwtConfig {
192 self.config
193 }
194}
195
196pub struct SecureJwtValidator {
197 config: SecureJwtConfig,
198 revoked_tokens: std::sync::Mutex<std::collections::HashMap<String, std::time::SystemTime>>,
203 on_revoke: std::sync::Mutex<Option<Box<dyn Fn(&str) + Send + Sync>>>,
209}
210
211impl std::fmt::Debug for SecureJwtValidator {
212 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213 f.debug_struct("SecureJwtValidator")
214 .field("config", &self.config)
215 .field("revoked_tokens", &self.revoked_tokens)
216 .field(
217 "on_revoke",
218 &self.on_revoke.lock().ok().map(|g| g.is_some()),
219 )
220 .finish()
221 }
222}
223
224impl SecureJwtValidator {
225 pub fn new(config: SecureJwtConfig) -> Result<Self> {
226 let has_hmac = config
228 .allowed_algorithms
229 .iter()
230 .any(|a| is_hmac_algorithm(*a));
231 let has_rsa = config.allowed_algorithms.iter().any(|a| {
232 matches!(
233 a,
234 Algorithm::RS256
235 | Algorithm::RS384
236 | Algorithm::RS512
237 | Algorithm::PS256
238 | Algorithm::PS384
239 | Algorithm::PS512
240 )
241 });
242 let has_ec = config
243 .allowed_algorithms
244 .iter()
245 .any(|a| matches!(a, Algorithm::ES256 | Algorithm::ES384));
246 let has_eddsa = config
247 .allowed_algorithms
248 .iter()
249 .any(|a| matches!(a, Algorithm::EdDSA));
250
251 if has_hmac {
252 #[cfg(not(test))]
253 if config.jwt_secret.len() < 32 {
254 return Err(AuthError::Configuration {
255 message: "SecureJwtConfig::jwt_secret must be at least 32 characters \
256 when HMAC algorithms are enabled"
257 .to_string(),
258 help: Some(
259 "Provide a cryptographically random secret unique to your deployment"
260 .to_string(),
261 ),
262 docs_url: None,
263 source: None,
264 suggested_fix: None,
265 });
266 }
267 }
268 if has_rsa && config.rsa_public_key_pem.is_none() {
269 return Err(AuthError::Configuration {
270 message: "SecureJwtConfig::rsa_public_key_pem must be set when RSA/PS algorithms are enabled".to_string(),
271 help: Some("Set rsa_public_key_pem in SecureJwtConfig".to_string()),
272 docs_url: None,
273 source: None,
274 suggested_fix: None,
275 });
276 }
277 if has_ec && config.ec_public_key_pem.is_none() {
278 return Err(AuthError::Configuration {
279 message:
280 "SecureJwtConfig::ec_public_key_pem must be set when EC algorithms are enabled"
281 .to_string(),
282 help: Some("Set ec_public_key_pem in SecureJwtConfig".to_string()),
283 docs_url: None,
284 source: None,
285 suggested_fix: None,
286 });
287 }
288 if has_eddsa && config.ed_public_key_pem.is_none() {
289 return Err(AuthError::Configuration {
290 message: "SecureJwtConfig::ed_public_key_pem must be set when EdDSA is enabled"
291 .to_string(),
292 help: Some("Set ed_public_key_pem in SecureJwtConfig".to_string()),
293 docs_url: None,
294 source: None,
295 suggested_fix: None,
296 });
297 }
298
299 Ok(Self {
300 config,
301 revoked_tokens: std::sync::Mutex::new(std::collections::HashMap::new()),
302 on_revoke: std::sync::Mutex::new(None),
303 })
304 }
305
306 pub fn set_on_revoke<F>(&self, callback: F)
321 where
322 F: Fn(&str) + Send + Sync + 'static,
323 {
324 let mut guard = match self.on_revoke.lock() {
325 Ok(g) => g,
326 Err(poisoned) => poisoned.into_inner(),
327 };
328 *guard = Some(Box::new(callback));
329 }
330
331 pub fn get_decoding_key(&self) -> jsonwebtoken::DecodingKey {
335 jsonwebtoken::DecodingKey::from_secret(self.config.jwt_secret.as_bytes())
336 }
337
338 pub fn get_encoding_key(&self) -> jsonwebtoken::EncodingKey {
340 jsonwebtoken::EncodingKey::from_secret(self.config.jwt_secret.as_bytes())
341 }
342
343 fn decoding_key_for_algorithm(&self, alg: Algorithm) -> Result<DecodingKey> {
345 match alg {
346 Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => {
347 Ok(DecodingKey::from_secret(self.config.jwt_secret.as_bytes()))
348 }
349 Algorithm::RS256
350 | Algorithm::RS384
351 | Algorithm::RS512
352 | Algorithm::PS256
353 | Algorithm::PS384
354 | Algorithm::PS512 => {
355 let pem = self.config.rsa_public_key_pem.as_deref().ok_or_else(|| {
356 AuthError::Configuration {
357 message: "RSA public key PEM not configured".to_string(),
358 help: Some(
359 "Set rsa_public_key_pem in SecureJwtConfig for RSA/PS algorithms"
360 .to_string(),
361 ),
362 docs_url: None,
363 source: None,
364 suggested_fix: None,
365 }
366 })?;
367 DecodingKey::from_rsa_pem(pem.as_bytes()).map_err(|e| AuthError::Configuration {
368 message: format!("Invalid RSA public key PEM: {e}"),
369 help: None,
370 docs_url: None,
371 source: None,
372 suggested_fix: None,
373 })
374 }
375 Algorithm::ES256 | Algorithm::ES384 => {
376 let pem = self.config.ec_public_key_pem.as_deref().ok_or_else(|| {
377 AuthError::Configuration {
378 message: "EC public key PEM not configured".to_string(),
379 help: Some(
380 "Set ec_public_key_pem in SecureJwtConfig for EC algorithms"
381 .to_string(),
382 ),
383 docs_url: None,
384 source: None,
385 suggested_fix: None,
386 }
387 })?;
388 DecodingKey::from_ec_pem(pem.as_bytes()).map_err(|e| AuthError::Configuration {
389 message: format!("Invalid EC public key PEM: {e}"),
390 help: None,
391 docs_url: None,
392 source: None,
393 suggested_fix: None,
394 })
395 }
396 Algorithm::EdDSA => {
397 let pem = self.config.ed_public_key_pem.as_deref().ok_or_else(|| {
398 AuthError::Configuration {
399 message: "Ed25519 public key PEM not configured".to_string(),
400 help: Some(
401 "Set ed_public_key_pem in SecureJwtConfig for EdDSA".to_string(),
402 ),
403 docs_url: None,
404 source: None,
405 suggested_fix: None,
406 }
407 })?;
408 DecodingKey::from_ed_pem(pem.as_bytes()).map_err(|e| AuthError::Configuration {
409 message: format!("Invalid Ed25519 public key PEM: {e}"),
410 help: None,
411 docs_url: None,
412 source: None,
413 suggested_fix: None,
414 })
415 }
416 }
417 }
418
419 pub fn validate(&self, token: &str) -> Result<SecureJwtClaims> {
428 let header = jsonwebtoken::decode_header(token)
430 .map_err(|e| AuthError::Unauthorized(format!("Invalid JWT header: {e}")))?;
431
432 if !self.config.allowed_algorithms.contains(&header.alg) {
434 return Err(AuthError::Unauthorized(format!(
435 "Token algorithm {:?} is not permitted; allowed: {:?}",
436 header.alg, self.config.allowed_algorithms
437 )));
438 }
439
440 let decoding_key = self.decoding_key_for_algorithm(header.alg)?;
442
443 let mut validation = jsonwebtoken::Validation::new(header.alg);
445 validation.algorithms = self.config.allowed_algorithms.clone();
446 validation.leeway = self.config.clock_skew.as_secs();
447
448 validation.validate_exp = true;
450
451 validation.validate_nbf = self.config.validate_nbf;
453
454 if !self.config.required_audiences.is_empty() {
456 validation.set_audience(
457 &self
458 .config
459 .required_audiences
460 .iter()
461 .collect::<Vec<&String>>(),
462 );
463 } else {
464 validation.validate_aud = false;
465 }
466
467 if !self.config.required_issuers.is_empty() {
469 validation.set_issuer(
470 &self
471 .config
472 .required_issuers
473 .iter()
474 .collect::<Vec<&String>>(),
475 );
476 }
477
478 let token_data = jsonwebtoken::decode::<SecureJwtClaims>(token, &decoding_key, &validation)
480 .map_err(|e| AuthError::Unauthorized(format!("JWT validation failed: {e}")))?;
481
482 let claims = token_data.claims;
483
484 if self.is_token_revoked(&claims.jti)? {
488 return Err(AuthError::Unauthorized("Token is revoked".to_string()));
489 }
490
491 let token_lifetime = claims.exp.saturating_sub(claims.iat);
493 if token_lifetime > 0 && (token_lifetime as u64) > self.config.max_token_lifetime.as_secs()
494 {
495 return Err(AuthError::Unauthorized(format!(
496 "Token lifetime ({token_lifetime}s) exceeds maximum allowed ({}s)",
497 self.config.max_token_lifetime.as_secs()
498 )));
499 }
500
501 if self.config.require_jti && claims.jti.is_empty() {
503 return Err(AuthError::Unauthorized(
504 "Token missing required JTI claim".to_string(),
505 ));
506 }
507
508 if !self.config.allowed_token_types.is_empty() && !claims.typ.is_empty() {
510 if !self.config.allowed_token_types.contains(&claims.typ) {
511 return Err(AuthError::Unauthorized(format!(
512 "Token type '{}' is not permitted",
513 claims.typ
514 )));
515 }
516 }
517
518 Ok(claims)
519 }
520
521 pub fn validate_token(
526 &self,
527 token: &str,
528 decoding_key: &DecodingKey,
529 ) -> Result<SecureJwtClaims> {
530 let header = jsonwebtoken::decode_header(token)
531 .map_err(|e| AuthError::Unauthorized(format!("Invalid JWT header: {e}")))?;
532
533 if !self.config.allowed_algorithms.contains(&header.alg) {
534 return Err(AuthError::Unauthorized(format!(
535 "Token algorithm {:?} is not permitted; allowed: {:?}",
536 header.alg, self.config.allowed_algorithms
537 )));
538 }
539
540 let mut validation = jsonwebtoken::Validation::new(header.alg);
541 validation.algorithms = self.config.allowed_algorithms.clone();
542 validation.leeway = self.config.clock_skew.as_secs();
543 validation.validate_exp = true;
544 validation.validate_nbf = self.config.validate_nbf;
545
546 if !self.config.required_audiences.is_empty() {
547 validation.set_audience(
548 &self
549 .config
550 .required_audiences
551 .iter()
552 .collect::<Vec<&String>>(),
553 );
554 } else {
555 validation.validate_aud = false;
556 }
557
558 if !self.config.required_issuers.is_empty() {
559 validation.set_issuer(
560 &self
561 .config
562 .required_issuers
563 .iter()
564 .collect::<Vec<&String>>(),
565 );
566 }
567
568 let token_data = jsonwebtoken::decode::<SecureJwtClaims>(token, decoding_key, &validation)
569 .map_err(|e| AuthError::Unauthorized(format!("JWT validation failed: {e}")))?;
570
571 let claims = token_data.claims;
572
573 if self.is_token_revoked(&claims.jti)? {
574 return Err(AuthError::Unauthorized("Token is revoked".to_string()));
575 }
576
577 let token_lifetime = claims.exp.saturating_sub(claims.iat);
578 if token_lifetime > 0 && (token_lifetime as u64) > self.config.max_token_lifetime.as_secs()
579 {
580 return Err(AuthError::Unauthorized(format!(
581 "Token lifetime ({token_lifetime}s) exceeds maximum allowed ({}s)",
582 self.config.max_token_lifetime.as_secs()
583 )));
584 }
585
586 if self.config.require_jti && claims.jti.is_empty() {
587 return Err(AuthError::Unauthorized(
588 "Token missing required JTI claim".to_string(),
589 ));
590 }
591
592 if !self.config.allowed_token_types.is_empty() && !claims.typ.is_empty() {
593 if !self.config.allowed_token_types.contains(&claims.typ) {
594 return Err(AuthError::Unauthorized(format!(
595 "Token type '{}' is not permitted",
596 claims.typ
597 )));
598 }
599 }
600
601 Ok(claims)
602 }
603
604 pub fn is_token_revoked(&self, jti: &str) -> Result<bool> {
610 let revoked_tokens = self.revoked_tokens.lock().map_err(|_| {
611 AuthError::internal("Lock poisoned — a prior thread panicked while holding this lock")
612 })?;
613 Ok(revoked_tokens.contains_key(jti))
614 }
615
616 pub fn revoke_token(&self, jti: &str) -> Result<()> {
626 {
627 let mut revoked_tokens = self.revoked_tokens.lock().map_err(|_| {
628 AuthError::internal(
629 "Lock poisoned — a prior thread panicked while holding this lock",
630 )
631 })?;
632 revoked_tokens.insert(jti.to_string(), std::time::SystemTime::now());
633 }
634 if let Some(ref cb) = *self.on_revoke.lock().map_err(|_| {
637 AuthError::internal("Lock poisoned — a prior thread panicked while holding this lock")
638 })? {
639 cb(jti);
640 }
641 Ok(())
642 }
643
644 pub fn cleanup_revoked_tokens(&self, expired_cutoff: std::time::SystemTime) -> Result<()> {
654 const MAX_REVOKED_TOKENS: usize = 10_000;
655 let mut revoked_tokens = self.revoked_tokens.lock().map_err(|_| {
656 AuthError::internal("Lock poisoned — a prior thread panicked while holding this lock")
657 })?;
658
659 revoked_tokens.retain(|_, inserted_at| *inserted_at >= expired_cutoff);
662
663 if revoked_tokens.len() > MAX_REVOKED_TOKENS {
666 let target_len = MAX_REVOKED_TOKENS * 3 / 4;
667 let mut by_age: Vec<(String, std::time::SystemTime)> = revoked_tokens.drain().collect();
668 by_age.sort_unstable_by_key(|(_, t)| *t);
669 for (jti, inserted_at) in by_age.into_iter().rev().take(target_len) {
671 revoked_tokens.insert(jti, inserted_at);
672 }
673 tracing::warn!(
674 "Revoked token list exceeded {} entries; oldest entries were evicted.",
675 MAX_REVOKED_TOKENS
676 );
677 }
678
679 Ok(())
680 }
681}
682
683#[cfg(test)]
684mod tests {
685 use super::*;
686 use jsonwebtoken::{Algorithm, EncodingKey, Header};
687
688 fn test_config() -> SecureJwtConfig {
689 SecureJwtConfig {
690 jwt_secret: "a]test_secret_that_is_longer_than_32_chars_for_security!".to_string(),
691 ..SecureJwtConfig::default()
692 }
693 }
694
695 fn issue_token(config: &SecureJwtConfig, claims: &SecureJwtClaims) -> String {
696 let key = EncodingKey::from_secret(config.jwt_secret.as_bytes());
697 jsonwebtoken::encode(&Header::new(Algorithm::HS256), claims, &key).unwrap()
698 }
699
700 fn valid_claims() -> SecureJwtClaims {
701 let now = chrono::Utc::now().timestamp();
702 SecureJwtClaims {
703 sub: "user123".to_string(),
704 iss: "auth-framework".to_string(),
705 aud: "test".to_string(),
706 exp: now + 600,
707 nbf: now - 10,
708 iat: now,
709 jti: uuid::Uuid::new_v4().to_string(),
710 scope: "read".to_string(),
711 typ: "access".to_string(),
712 sid: None,
713 client_id: None,
714 auth_ctx_hash: None,
715 }
716 }
717
718 #[test]
719 fn test_default_config_generates_random_secret() {
720 let c1 = SecureJwtConfig::default();
721 let c2 = SecureJwtConfig::default();
722 assert_ne!(c1.jwt_secret, c2.jwt_secret);
723 assert!(c1.jwt_secret.len() >= 32);
724 }
725
726 #[test]
727 fn test_builder_fluent_api() {
728 let config = SecureJwtConfig::builder()
729 .with_secret("a]test_secret_that_is_longer_than_32_chars_for_security!")
730 .require_issuer("my-issuer")
731 .require_audience("my-aud")
732 .with_max_lifetime(Duration::from_secs(7200))
733 .with_clock_skew(Duration::from_secs(60))
734 .require_jti(false)
735 .build();
736
737 assert!(config.required_issuers.contains("my-issuer"));
738 assert!(config.required_audiences.contains("my-aud"));
739 assert_eq!(config.max_token_lifetime, Duration::from_secs(7200));
740 assert_eq!(config.clock_skew, Duration::from_secs(60));
741 assert!(!config.require_jti);
742 }
743
744 #[test]
745 fn test_validate_valid_token() {
746 let config = test_config();
747 let claims = valid_claims();
748 let token = issue_token(&config, &claims);
749 let validator = SecureJwtValidator::new(config).unwrap();
750 let result = validator.validate(&token).unwrap();
751 assert_eq!(result.sub, "user123");
752 assert_eq!(result.iss, "auth-framework");
753 }
754
755 #[test]
756 fn test_validate_rejects_expired_token() {
757 let config = test_config();
758 let mut claims = valid_claims();
759 claims.exp = chrono::Utc::now().timestamp() - 3600;
760 claims.iat = claims.exp - 600;
761 let token = issue_token(&config, &claims);
762 let validator = SecureJwtValidator::new(config).unwrap();
763 assert!(validator.validate(&token).is_err());
764 }
765
766 #[test]
767 fn test_validate_rejects_wrong_issuer() {
768 let config = test_config();
769 let mut claims = valid_claims();
770 claims.iss = "evil-issuer".to_string();
771 let token = issue_token(&config, &claims);
772 let validator = SecureJwtValidator::new(config).unwrap();
773 assert!(validator.validate(&token).is_err());
774 }
775
776 #[test]
777 fn test_revoke_and_check() {
778 let config = test_config();
779 let validator = SecureJwtValidator::new(config).unwrap();
780 let jti = "test-jti-123";
781 assert!(!validator.is_token_revoked(jti).unwrap());
782 validator.revoke_token(jti).unwrap();
783 assert!(validator.is_token_revoked(jti).unwrap());
784 }
785
786 #[test]
787 fn test_revoked_token_rejected() {
788 let config = test_config();
789 let claims = valid_claims();
790 let jti = claims.jti.clone();
791 let token = issue_token(&config, &claims);
792 let validator = SecureJwtValidator::new(config).unwrap();
793 validator.revoke_token(&jti).unwrap();
794 assert!(validator.validate(&token).is_err());
795 }
796
797 #[test]
798 fn test_cleanup_removes_old_entries() {
799 let config = test_config();
800 let validator = SecureJwtValidator::new(config).unwrap();
801 validator.revoke_token("old-jti").unwrap();
802 let future = std::time::SystemTime::now() + Duration::from_secs(3600);
804 validator.cleanup_revoked_tokens(future).unwrap();
805 assert!(!validator.is_token_revoked("old-jti").unwrap());
806 }
807
808 #[test]
809 fn test_on_revoke_callback() {
810 let config = test_config();
811 let validator = SecureJwtValidator::new(config).unwrap();
812 let captured = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
813 let captured_clone = captured.clone();
814 validator.set_on_revoke(move |jti| {
815 captured_clone.lock().unwrap().push(jti.to_string());
816 });
817 validator.revoke_token("cb-jti-1").unwrap();
818 validator.revoke_token("cb-jti-2").unwrap();
819 let jtis = captured.lock().unwrap();
820 assert_eq!(jtis.len(), 2);
821 assert!(jtis.contains(&"cb-jti-1".to_string()));
822 assert!(jtis.contains(&"cb-jti-2".to_string()));
823 }
824
825 #[test]
826 fn test_rejects_disallowed_algorithm() {
827 let config = test_config();
828 let claims = valid_claims();
829 let key = EncodingKey::from_secret(config.jwt_secret.as_bytes());
831 let token =
832 jsonwebtoken::encode(&Header::new(Algorithm::HS384), &claims, &key).unwrap();
833 let validator = SecureJwtValidator::new(config).unwrap();
834 assert!(validator.validate(&token).is_err());
835 }
836
837 #[test]
838 fn test_rejects_excessive_lifetime() {
839 let mut config = test_config();
840 config.max_token_lifetime = Duration::from_secs(300);
841 let mut claims = valid_claims();
842 let now = chrono::Utc::now().timestamp();
843 claims.iat = now;
844 claims.exp = now + 600; let token = issue_token(&config, &claims);
846 let validator = SecureJwtValidator::new(config).unwrap();
847 assert!(validator.validate(&token).is_err());
848 }
849
850 #[test]
851 fn test_missing_rsa_key_rejected() {
852 let mut config = test_config();
853 config.allowed_algorithms = vec![Algorithm::RS256];
854 let result = SecureJwtValidator::new(config);
855 assert!(result.is_err());
856 }
857}