Skip to main content

helios_auth/jti/
memory.rs

1use std::time::Duration;
2
3use async_trait::async_trait;
4use chrono::{DateTime, Utc};
5use moka::future::Cache;
6
7use super::JtiCache;
8use crate::error::AuthError;
9
10/// In-memory JTI cache backed by moka.
11///
12/// Uses moka's time-to-live (TTL) to automatically evict entries
13/// after tokens expire, preventing unbounded growth.
14pub struct InMemoryJtiCache {
15    cache: Cache<String, ()>,
16}
17
18impl InMemoryJtiCache {
19    /// Create a new in-memory JTI cache.
20    ///
21    /// Entries are evicted after 1 hour (covers typical token lifetimes).
22    /// Max capacity prevents unbounded growth.
23    pub fn new() -> Self {
24        let cache = Cache::builder()
25            .max_capacity(100_000)
26            .time_to_live(Duration::from_secs(3600))
27            .build();
28        Self { cache }
29    }
30}
31
32impl Default for InMemoryJtiCache {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38#[async_trait]
39impl JtiCache for InMemoryJtiCache {
40    async fn check_and_store(
41        &self,
42        jti: &str,
43        _expires_at: DateTime<Utc>,
44    ) -> Result<bool, AuthError> {
45        // Check if already present
46        if self.cache.get(jti).await.is_some() {
47            return Ok(true); // replay
48        }
49
50        // Store the JTI (TTL is set at cache level)
51        self.cache.insert(jti.to_string(), ()).await;
52
53        Ok(false) // not a replay
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60    use chrono::Duration as ChronoDuration;
61
62    #[tokio::test]
63    async fn test_new_jti_not_replay() {
64        let cache = InMemoryJtiCache::new();
65        let expires = Utc::now() + ChronoDuration::hours(1);
66
67        let replay = cache.check_and_store("jti-1", expires).await.unwrap();
68        assert!(!replay);
69    }
70
71    #[tokio::test]
72    async fn test_duplicate_jti_is_replay() {
73        let cache = InMemoryJtiCache::new();
74        let expires = Utc::now() + ChronoDuration::hours(1);
75
76        let first = cache.check_and_store("jti-2", expires).await.unwrap();
77        assert!(!first);
78
79        let second = cache.check_and_store("jti-2", expires).await.unwrap();
80        assert!(second);
81    }
82
83    #[tokio::test]
84    async fn test_different_jtis_independent() {
85        let cache = InMemoryJtiCache::new();
86        let expires = Utc::now() + ChronoDuration::hours(1);
87
88        let a = cache.check_and_store("jti-a", expires).await.unwrap();
89        assert!(!a);
90
91        let b = cache.check_and_store("jti-b", expires).await.unwrap();
92        assert!(!b);
93    }
94}