use std::collections::HashMap;
use std::fmt::Debug;
use std::sync::Arc;
use tokio::sync::Mutex;
use crate::VerifierError;
use crate::time_utils::unix_now_secs_i64 as unix_now;
#[async_trait::async_trait]
pub trait DpopReplayStore: Debug + Send + Sync {
async fn check_and_store(&self, jti: &str, expires_at: i64) -> Result<bool, VerifierError>;
}
#[derive(Debug, Default, Clone)]
pub struct InMemoryDpopReplayStore {
inner: Arc<Mutex<HashMap<String, i64>>>,
}
impl InMemoryDpopReplayStore {
pub fn new() -> Self {
Self::default()
}
pub async fn len(&self) -> usize {
let entries = self.inner.lock().await;
let now = unix_now();
entries.values().filter(|expires| **expires > now).count()
}
pub async fn is_empty(&self) -> bool {
self.len().await == 0
}
}
#[async_trait::async_trait]
impl DpopReplayStore for InMemoryDpopReplayStore {
async fn check_and_store(&self, jti: &str, expires_at: i64) -> Result<bool, VerifierError> {
let mut entries = self.inner.lock().await;
let now = unix_now();
entries.retain(|_, exp| *exp > now);
if entries.contains_key(jti) {
return Ok(false);
}
entries.insert(jti.to_string(), expires_at);
Ok(true)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn first_seen_jti_is_stored() {
let store = InMemoryDpopReplayStore::new();
let stored = store
.check_and_store("jti-1", unix_now() + 60)
.await
.unwrap();
assert!(stored);
}
#[tokio::test]
async fn duplicate_jti_is_rejected() {
let store = InMemoryDpopReplayStore::new();
let first = store
.check_and_store("jti-1", unix_now() + 60)
.await
.unwrap();
let second = store
.check_and_store("jti-1", unix_now() + 60)
.await
.unwrap();
assert!(first);
assert!(!second);
}
#[tokio::test]
async fn expired_entries_are_evicted() {
let store = InMemoryDpopReplayStore::new();
store
.check_and_store("jti-old", unix_now() - 1)
.await
.unwrap();
store
.check_and_store("jti-new", unix_now() + 60)
.await
.unwrap();
let stored_again = store
.check_and_store("jti-old", unix_now() + 60)
.await
.unwrap();
assert!(stored_again);
}
#[tokio::test]
async fn distinct_jtis_coexist() {
let store = InMemoryDpopReplayStore::new();
store.check_and_store("a", unix_now() + 60).await.unwrap();
store.check_and_store("b", unix_now() + 60).await.unwrap();
assert_eq!(store.len().await, 2);
}
}