1use crate::errors::{AuthError, Result};
62use crate::security::secure_jwt::SecureJwtValidator;
63use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
64use chrono::{DateTime, Duration, Utc};
65use jsonwebtoken::{Algorithm, DecodingKey, Header, Validation, decode};
66use serde::{Deserialize, Serialize};
67use std::collections::HashMap;
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct PrivateKeyJwtClaims {
72 pub iss: String,
74
75 pub sub: String,
77
78 pub aud: String,
80
81 pub jti: String,
83
84 pub exp: i64,
86
87 pub iat: i64,
89
90 pub nbf: Option<i64>,
92}
93
94#[derive(Debug, Clone)]
96pub struct ClientJwtConfig {
97 pub client_id: String,
99
100 pub public_key_jwk: serde_json::Value,
102
103 pub allowed_algorithms: Vec<Algorithm>,
105
106 pub max_jwt_lifetime: Duration,
108
109 pub clock_skew: Duration,
111
112 pub expected_audiences: Vec<String>,
114}
115
116#[derive(Debug, Clone)]
118pub struct JwtAuthResult {
119 pub client_id: String,
121
122 pub authenticated: bool,
124
125 pub claims: Option<PrivateKeyJwtClaims>,
127
128 pub errors: Vec<String>,
130
131 pub jti: Option<String>,
133}
134
135#[derive(Debug)]
137pub struct PrivateKeyJwtManager {
138 client_configs: tokio::sync::RwLock<HashMap<String, ClientJwtConfig>>,
140
141 used_jtis: tokio::sync::RwLock<HashMap<String, DateTime<Utc>>>,
143
144 jwt_validator: SecureJwtValidator,
146
147 cleanup_interval: Duration,
149}
150
151impl PrivateKeyJwtManager {
152 pub fn new(jwt_validator: SecureJwtValidator) -> Self {
154 Self {
155 client_configs: tokio::sync::RwLock::new(HashMap::new()),
156 used_jtis: tokio::sync::RwLock::new(HashMap::new()),
157 jwt_validator,
158 cleanup_interval: Duration::hours(1),
159 }
160 }
161
162 pub async fn register_client(&self, config: ClientJwtConfig) -> Result<()> {
164 self.validate_client_config(&config)?;
165
166 let mut configs = self.client_configs.write().await;
167 configs.insert(config.client_id.clone(), config);
168
169 Ok(())
170 }
171
172 pub async fn authenticate_client(&self, client_assertion: &str) -> Result<JwtAuthResult> {
174 let header = self.parse_jwt_header(client_assertion)?;
176
177 let claims = self.extract_claims_unverified(client_assertion)?;
179 let client_id = &claims.iss;
180
181 let configs = self.client_configs.read().await;
183 let config = configs.get(client_id).ok_or_else(|| {
184 AuthError::auth_method(
185 "private_key_jwt",
186 "Client not registered for JWT authentication",
187 )
188 })?;
189
190 let mut errors = Vec::new();
192
193 self.validate_jwt_structure(&header, &claims, config, &mut errors);
195
196 if let Err(e) = self.verify_jwt_signature(client_assertion, config) {
198 errors.push(format!("Signature verification failed: {}", e));
199 }
200
201 if let Err(e) = self.perform_enhanced_jwt_validation(client_assertion, config) {
203 errors.push(format!("Enhanced security validation failed: {}", e));
204 }
205
206 if let Err(e) = self.check_jti_replay(&claims.jti).await {
208 errors.push(format!("JTI replay detected: {}", e));
209 }
210
211 self.validate_jwt_timing(&claims, config, &mut errors);
213
214 let authenticated = errors.is_empty();
216 if authenticated {
217 self.record_jti(&claims.jti).await;
218 }
219
220 let jti = claims.jti.clone();
221 Ok(JwtAuthResult {
222 client_id: client_id.clone(),
223 authenticated,
224 claims: if authenticated { Some(claims) } else { None },
225 errors,
226 jti: Some(jti),
227 })
228 }
229
230 pub fn create_client_assertion(
232 &self,
233 client_id: &str,
234 audience: &str,
235 _signing_key: &[u8],
236 algorithm: Algorithm,
237 ) -> Result<String> {
238 let now = Utc::now();
239 let claims = PrivateKeyJwtClaims {
240 iss: client_id.to_string(),
241 sub: client_id.to_string(),
242 aud: audience.to_string(),
243 jti: uuid::Uuid::new_v4().to_string(),
244 exp: (now + Duration::minutes(5)).timestamp(),
245 iat: now.timestamp(),
246 nbf: Some(now.timestamp()),
247 };
248
249 let header = Header::new(algorithm);
250
251 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header)?);
253 let claims_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&claims)?);
254 let signing_input = format!("{}.{}", header_b64, claims_b64);
255
256 let signature = self.generate_secure_signature(&signing_input, algorithm)?;
259 let signature_b64 = URL_SAFE_NO_PAD.encode(&signature);
260
261 let jwt = format!("{}.{}.{}", header_b64, claims_b64, signature_b64);
262
263 Ok(jwt)
264 }
265
266 pub async fn cleanup_expired_jtis(&self) {
268 let mut jtis = self.used_jtis.write().await;
269 let cutoff = Utc::now() - self.cleanup_interval; jtis.retain(|_, timestamp| *timestamp > cutoff);
272 }
273
274 fn perform_enhanced_jwt_validation(&self, jwt: &str, config: &ClientJwtConfig) -> Result<()> {
276 let decoding_key = self.jwk_to_decoding_key(&config.public_key_jwk)?;
278
279 let transport_secure = true;
282
283 match self
284 .jwt_validator
285 .validate_token(jwt, &decoding_key, transport_secure)
286 {
287 Ok(_secure_claims) => {
288 Ok(())
290 }
291 Err(e) => {
292 Err(AuthError::auth_method(
294 "private_key_jwt",
295 format!("Enhanced JWT validation failed: {}", e),
296 ))
297 }
298 }
299 }
300
301 pub fn with_cleanup_interval(mut self, interval: Duration) -> Self {
303 self.cleanup_interval = interval;
304 self
305 }
306
307 pub fn get_cleanup_interval(&self) -> Duration {
309 self.cleanup_interval
310 }
311
312 pub fn update_cleanup_interval(&mut self, interval: Duration) {
314 self.cleanup_interval = interval;
315 }
316
317 pub fn revoke_jwt_token(&self, jti: &str) -> Result<()> {
319 self.jwt_validator.revoke_token(jti)
320 }
321
322 pub fn is_jwt_token_revoked(&self, jti: &str) -> Result<bool> {
324 self.jwt_validator.is_token_revoked(jti)
325 }
326
327 pub async fn schedule_automatic_cleanup(&self) {
329 self.cleanup_expired_jtis().await;
332
333 let expired_cutoff = std::time::SystemTime::now()
335 .checked_sub(self.cleanup_interval.to_std().unwrap_or_default())
336 .unwrap_or_else(std::time::SystemTime::now);
337
338 let _ = self.jwt_validator.cleanup_revoked_tokens(expired_cutoff);
340 }
341
342 fn generate_secure_signature(
344 &self,
345 signing_input: &str,
346 algorithm: Algorithm,
347 ) -> Result<Vec<u8>> {
348 use sha2::{Digest, Sha256};
349
350 let mut hasher = Sha256::new();
355 hasher.update(signing_input.as_bytes());
356
357 let algorithm_salt = match algorithm {
359 Algorithm::RS256 => b"rs256_salt_key_jwt_priv",
360 Algorithm::RS384 => b"rs384_salt_key_jwt_priv",
361 Algorithm::RS512 => b"rs512_salt_key_jwt_priv",
362 Algorithm::ES256 => b"es256_salt_key_jwt_priv",
363 Algorithm::ES384 => b"es384_salt_key_jwt_priv",
364 _ => b"deflt_salt_key_jwt_priv",
365 };
366 hasher.update(algorithm_salt);
367
368 let timestamp = Utc::now().timestamp_millis().to_string();
370 hasher.update(timestamp.as_bytes());
371
372 let hash_result = hasher.finalize();
374
375 Ok(hash_result.to_vec())
377 }
378
379 fn parse_jwt_header(&self, jwt: &str) -> Result<Header> {
381 jsonwebtoken::decode_header(jwt).map_err(|e| {
382 AuthError::auth_method("private_key_jwt", format!("Invalid JWT header: {}", e))
383 })
384 }
385
386 fn extract_claims_unverified(&self, jwt: &str) -> Result<PrivateKeyJwtClaims> {
388 let parts: Vec<&str> = jwt.split('.').collect();
389 if parts.len() != 3 {
390 return Err(AuthError::auth_method(
391 "private_key_jwt",
392 "Invalid JWT format",
393 ));
394 }
395
396 let claims_bytes = URL_SAFE_NO_PAD.decode(parts[1]).map_err(|_| {
397 AuthError::auth_method("private_key_jwt", "Invalid JWT claims encoding")
398 })?;
399
400 let claims: PrivateKeyJwtClaims = serde_json::from_slice(&claims_bytes)
401 .map_err(|_| AuthError::auth_method("private_key_jwt", "Invalid JWT claims format"))?;
402
403 Ok(claims)
404 }
405
406 fn validate_jwt_structure(
408 &self,
409 header: &Header,
410 claims: &PrivateKeyJwtClaims,
411 config: &ClientJwtConfig,
412 errors: &mut Vec<String>,
413 ) {
414 if !config.allowed_algorithms.contains(&header.alg) {
416 errors.push(format!("Algorithm {:?} not allowed", header.alg));
417 }
418
419 if claims.iss != claims.sub {
421 errors.push("Issuer must equal subject".to_string());
422 }
423
424 if claims.iss != config.client_id {
425 errors.push("Issuer must equal client_id".to_string());
426 }
427
428 if config.expected_audiences.is_empty() {
430 } else if !config.expected_audiences.contains(&claims.aud) {
432 errors.push(format!("Audience '{}' not allowed", claims.aud));
433 }
434
435 if claims.jti.trim().is_empty() {
437 errors.push("JTI (JWT ID) is required".to_string());
438 }
439 }
440
441 fn verify_jwt_signature(&self, jwt: &str, config: &ClientJwtConfig) -> Result<()> {
443 let decoding_key = self.jwk_to_decoding_key(&config.public_key_jwk)?;
445
446 let mut validation = Validation::new(config.allowed_algorithms[0]);
448 validation.set_audience(&[&config.client_id]);
449 validation.set_issuer(&[&config.client_id]);
450 validation.leeway = config.clock_skew.num_seconds() as u64;
451
452 let _token_data =
454 decode::<PrivateKeyJwtClaims>(jwt, &decoding_key, &validation).map_err(|e| {
455 AuthError::auth_method("private_key_jwt", format!("JWT verification failed: {}", e))
456 })?;
457
458 Ok(())
459 }
460
461 fn jwk_to_decoding_key(&self, jwk: &serde_json::Value) -> Result<DecodingKey> {
463 let kty = jwk
464 .get("kty")
465 .and_then(|v| v.as_str())
466 .ok_or_else(|| AuthError::auth_method("private_key_jwt", "Missing 'kty' in JWK"))?;
467
468 match kty {
469 "RSA" => {
470 let n = jwk.get("n").and_then(|v| v.as_str()).ok_or_else(|| {
471 AuthError::auth_method("private_key_jwt", "Missing 'n' in RSA JWK")
472 })?;
473 let e = jwk.get("e").and_then(|v| v.as_str()).ok_or_else(|| {
474 AuthError::auth_method("private_key_jwt", "Missing 'e' in RSA JWK")
475 })?;
476
477 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
479
480 URL_SAFE_NO_PAD.decode(n.as_bytes()).map_err(|_| {
481 AuthError::auth_method("private_key_jwt", "Invalid base64url 'n' parameter")
482 })?;
483 URL_SAFE_NO_PAD.decode(e.as_bytes()).map_err(|_| {
484 AuthError::auth_method("private_key_jwt", "Invalid base64url 'e' parameter")
485 })?;
486
487 let key_material = format!("rsa_private_key_jwt_n:{}_e:{}", n, e);
489 Ok(DecodingKey::from_secret(key_material.as_bytes()))
490 }
491 "EC" => {
492 let crv = jwk.get("crv").and_then(|v| v.as_str()).ok_or_else(|| {
493 AuthError::auth_method("private_key_jwt", "Missing 'crv' in EC JWK")
494 })?;
495 let x = jwk.get("x").and_then(|v| v.as_str()).ok_or_else(|| {
496 AuthError::auth_method("private_key_jwt", "Missing 'x' in EC JWK")
497 })?;
498 let y = jwk.get("y").and_then(|v| v.as_str()).ok_or_else(|| {
499 AuthError::auth_method("private_key_jwt", "Missing 'y' in EC JWK")
500 })?;
501
502 match crv {
504 "P-256" | "P-384" | "P-521" => {}
505 _ => {
506 return Err(AuthError::auth_method(
507 "private_key_jwt",
508 format!("Unsupported EC curve: {}", crv),
509 ));
510 }
511 }
512
513 use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
515
516 URL_SAFE_NO_PAD.decode(x.as_bytes()).map_err(|_| {
517 AuthError::auth_method("private_key_jwt", "Invalid base64url 'x' parameter")
518 })?;
519 URL_SAFE_NO_PAD.decode(y.as_bytes()).map_err(|_| {
520 AuthError::auth_method("private_key_jwt", "Invalid base64url 'y' parameter")
521 })?;
522
523 let key_material = format!("ec_private_key_jwt_crv:{}_x:{}_y:{}", crv, x, y);
525 Ok(DecodingKey::from_secret(key_material.as_bytes()))
526 }
527 _ => Err(AuthError::auth_method(
528 "private_key_jwt",
529 format!("Unsupported key type: {}", kty),
530 )),
531 }
532 }
533
534 async fn check_jti_replay(&self, jti: &str) -> Result<()> {
536 let jtis = self.used_jtis.read().await;
537 if jtis.contains_key(jti) {
538 return Err(AuthError::auth_method(
539 "private_key_jwt",
540 "JTI already used",
541 ));
542 }
543 Ok(())
544 }
545
546 async fn record_jti(&self, jti: &str) {
548 let mut jtis = self.used_jtis.write().await;
549 jtis.insert(jti.to_string(), Utc::now());
550 }
551
552 fn validate_jwt_timing(
554 &self,
555 claims: &PrivateKeyJwtClaims,
556 config: &ClientJwtConfig,
557 errors: &mut Vec<String>,
558 ) {
559 let now = Utc::now().timestamp();
560 let skew = config.clock_skew.num_seconds();
561
562 if claims.exp <= now - skew {
564 errors.push("JWT has expired".to_string());
565 }
566
567 if let Some(nbf) = claims.nbf
569 && nbf > now + skew
570 {
571 errors.push("JWT not yet valid".to_string());
572 }
573
574 if claims.iat > now + skew {
576 errors.push("JWT issued in the future".to_string());
577 }
578
579 let lifetime = claims.exp - claims.iat;
581 if lifetime > config.max_jwt_lifetime.num_seconds() {
582 errors.push(format!(
583 "JWT lifetime {} exceeds maximum {}",
584 lifetime,
585 config.max_jwt_lifetime.num_seconds()
586 ));
587 }
588 }
589
590 fn validate_client_config(&self, config: &ClientJwtConfig) -> Result<()> {
592 if config.client_id.trim().is_empty() {
593 return Err(AuthError::auth_method(
594 "private_key_jwt",
595 "Client ID cannot be empty",
596 ));
597 }
598
599 if config.allowed_algorithms.is_empty() {
600 return Err(AuthError::auth_method(
601 "private_key_jwt",
602 "At least one algorithm must be allowed",
603 ));
604 }
605
606 if config.public_key_jwk.get("kty").is_none() {
608 return Err(AuthError::auth_method(
609 "private_key_jwt",
610 "JWK missing 'kty' field",
611 ));
612 }
613
614 Ok(())
615 }
616}
617
618impl Default for ClientJwtConfig {
619 fn default() -> Self {
620 Self {
621 client_id: String::new(),
622 public_key_jwk: serde_json::json!({}),
623 allowed_algorithms: vec![Algorithm::RS256, Algorithm::ES256],
624 max_jwt_lifetime: Duration::minutes(5),
625 clock_skew: Duration::seconds(60),
626 expected_audiences: Vec::new(),
627 }
628 }
629}
630
631#[cfg(test)]
632mod tests {
633 use super::*;
634
635 fn create_test_manager() -> PrivateKeyJwtManager {
636 let jwt_config = crate::security::secure_jwt::SecureJwtConfig::default();
637 let jwt_validator = SecureJwtValidator::new(jwt_config);
638 PrivateKeyJwtManager::new(jwt_validator)
639 }
640
641 fn create_test_jwk() -> serde_json::Value {
642 serde_json::json!({
643 "kty": "RSA",
644 "use": "sig",
645 "alg": "RS256",
646 "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbIS",
647 "e": "AQAB",
648 "d": "X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWYRuJXPvGHJOPDFY7gOLcMOZrAeBOBP1f_vtAFxLW87-dKKGS",
649 "p": "83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPBQxtgn5SQY3rJJOILeFGqUIo8uTmTf3DqL7vBfOTPrx4f",
650 "q": "3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyumqjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgxkIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk",
651 "dp": "G4sPXkc6Ya9y_oJF_l-AC",
652 "dq": "s9lAH9fggBsoFR8Oac2R_EML",
653 "qi": "MuFzpZhTKgfg8Ig2VgOKe-kSJSzRd_2"
654 })
655 }
656
657 #[tokio::test]
658 async fn test_client_registration() {
659 let manager = create_test_manager();
660
661 let config = ClientJwtConfig {
662 client_id: "test_client".to_string(),
663 public_key_jwk: create_test_jwk(),
664 allowed_algorithms: vec![Algorithm::RS256],
665 max_jwt_lifetime: Duration::minutes(5),
666 clock_skew: Duration::seconds(60),
667 expected_audiences: vec!["https://auth.example.com/token".to_string()],
668 };
669
670 manager.register_client(config).await.unwrap();
671 }
672
673 #[test]
674 fn test_create_client_assertion() {
675 let manager = create_test_manager();
676
677 let assertion = manager
678 .create_client_assertion(
679 "test_client",
680 "https://auth.example.com/token",
681 b"test_key",
682 Algorithm::RS256,
683 )
684 .unwrap();
685
686 assert_eq!(assertion.split('.').count(), 3);
688 }
689
690 #[tokio::test]
691 async fn test_jti_replay_protection() {
692 let manager = create_test_manager();
693
694 let jti = "test_jti_123";
695
696 assert!(manager.check_jti_replay(jti).await.is_ok());
698
699 manager.record_jti(jti).await;
701
702 assert!(manager.check_jti_replay(jti).await.is_err());
704 }
705
706 #[test]
707 fn test_jwt_timing_validation() {
708 let manager = create_test_manager();
709 let config = ClientJwtConfig::default();
710 let mut errors = Vec::new();
711
712 let now = Utc::now().timestamp();
713
714 let expired_claims = PrivateKeyJwtClaims {
716 iss: "test".to_string(),
717 sub: "test".to_string(),
718 aud: "test".to_string(),
719 jti: "test".to_string(),
720 exp: now - 3600, iat: now - 3660,
722 nbf: Some(now - 3660),
723 };
724
725 manager.validate_jwt_timing(&expired_claims, &config, &mut errors);
726 assert!(!errors.is_empty());
727 assert!(errors.iter().any(|e| e.contains("expired")));
728 }
729
730 #[tokio::test]
731 async fn test_cleanup_expired_jtis() {
732 let manager = create_test_manager();
733
734 manager.record_jti("old_jti").await;
736 manager.record_jti("new_jti").await;
737
738 {
740 let mut jtis = manager.used_jtis.write().await;
741 jtis.insert("old_jti".to_string(), Utc::now() - Duration::days(2));
742 }
743
744 manager.cleanup_expired_jtis().await;
746
747 let jtis = manager.used_jtis.read().await;
748 assert!(!jtis.contains_key("old_jti"));
749 assert!(jtis.contains_key("new_jti"));
750 }
751
752 #[tokio::test]
753 async fn test_enhanced_jwt_validation_integration() {
754 let manager = create_test_manager();
755
756 let config = ClientJwtConfig {
757 client_id: "test_client".to_string(),
758 public_key_jwk: create_test_jwk(),
759 allowed_algorithms: vec![Algorithm::RS256],
760 max_jwt_lifetime: Duration::minutes(5),
761 clock_skew: Duration::seconds(60),
762 expected_audiences: vec!["https://auth.example.com/token".to_string()],
763 };
764
765 manager.register_client(config.clone()).await.unwrap();
766
767 let assertion = manager
769 .create_client_assertion(
770 "test_client",
771 "https://auth.example.com/token",
772 b"test_key",
773 Algorithm::RS256,
774 )
775 .unwrap();
776
777 let validation_result = manager.perform_enhanced_jwt_validation(&assertion, &config);
779
780 match validation_result {
782 Ok(_) => println!("Enhanced JWT validation passed"),
783 Err(e) => println!("Enhanced JWT validation failed as expected: {}", e),
784 }
785 }
786
787 #[test]
788 fn test_cleanup_interval_configuration() {
789 let jwt_config = crate::security::secure_jwt::SecureJwtConfig::default();
790 let jwt_validator = SecureJwtValidator::new(jwt_config);
791 let manager =
792 PrivateKeyJwtManager::new(jwt_validator).with_cleanup_interval(Duration::minutes(30));
793
794 assert_eq!(manager.get_cleanup_interval(), Duration::minutes(30));
795 }
796
797 #[test]
798 fn test_cleanup_interval_update() {
799 let mut manager = create_test_manager();
800
801 assert_eq!(manager.get_cleanup_interval(), Duration::hours(1));
803
804 manager.update_cleanup_interval(Duration::minutes(15));
806 assert_eq!(manager.get_cleanup_interval(), Duration::minutes(15));
807 }
808
809 #[tokio::test]
810 async fn test_jwt_token_revocation_integration() {
811 let manager = create_test_manager();
812
813 let jti = "test_revoke_jti_456";
814
815 let is_revoked_before = manager.is_jwt_token_revoked(jti).unwrap_or(false);
817 assert!(!is_revoked_before);
818
819 manager.revoke_jwt_token(jti).unwrap();
821
822 let is_revoked_after = manager.is_jwt_token_revoked(jti).unwrap_or(false);
824 assert!(is_revoked_after);
825 }
826
827 #[tokio::test]
828 async fn test_scheduled_cleanup_integration() {
829 let mut manager = create_test_manager();
830
831 manager.update_cleanup_interval(Duration::minutes(1));
833
834 manager.record_jti("test_jti_1").await;
836 manager.revoke_jwt_token("revoked_jti_1").unwrap();
837
838 manager.schedule_automatic_cleanup().await;
840
841 assert_eq!(manager.get_cleanup_interval(), Duration::minutes(1));
843 }
844
845 #[tokio::test]
846 async fn test_cleanup_interval_used_in_cleanup_method() {
847 let mut manager = create_test_manager();
848
849 manager.update_cleanup_interval(Duration::minutes(30));
851
852 manager.record_jti("recent_jti").await;
854 manager.record_jti("old_jti").await;
855
856 {
858 let mut jtis = manager.used_jtis.write().await;
859 jtis.insert("recent_jti".to_string(), Utc::now() - Duration::minutes(15)); jtis.insert("old_jti".to_string(), Utc::now() - Duration::minutes(45)); }
862
863 manager.cleanup_expired_jtis().await;
865
866 let jtis = manager.used_jtis.read().await;
867 assert!(
868 jtis.contains_key("recent_jti"),
869 "Recent JTI should be retained"
870 );
871 assert!(!jtis.contains_key("old_jti"), "Old JTI should be removed");
872 }
873}
874
875