use shared_state::ttl::TtlCache;
use std::time::Duration;
#[async_trait::async_trait]
pub trait CsrfStore: Send + Sync {
async fn store(&self, key: &str, nonce: &str, redirect_url: Option<String>) -> bool;
async fn retrieve_and_consume(&self, key: &str) -> Option<(String, Option<String>)>;
async fn cleanup_expired(&self);
}
pub struct MemoryCsrfStore {
entries: TtlCache<String, (String, Option<String>)>,
}
impl Default for MemoryCsrfStore {
fn default() -> Self {
Self::new()
}
}
impl MemoryCsrfStore {
pub fn new() -> Self {
Self::with_ttl(Duration::from_secs(600))
}
pub fn with_ttl(ttl: Duration) -> Self {
Self {
entries: TtlCache::new(ttl),
}
}
}
#[async_trait::async_trait]
impl CsrfStore for MemoryCsrfStore {
async fn store(&self, key: &str, nonce: &str, redirect_url: Option<String>) -> bool {
self.entries
.insert(key.to_string(), (nonce.to_string(), redirect_url));
true
}
async fn retrieve_and_consume(&self, key: &str) -> Option<(String, Option<String>)> {
self.entries.take_fresh(&key.to_string())
}
async fn cleanup_expired(&self) {
self.entries.cleanup();
}
}
pub enum CsrfStoreType {
Memory(MemoryCsrfStore),
}
impl Default for CsrfStoreType {
fn default() -> Self {
Self::Memory(MemoryCsrfStore::new())
}
}
impl CsrfStoreType {
pub fn new() -> Self {
Self::Memory(MemoryCsrfStore::new())
}
}
#[async_trait::async_trait]
impl CsrfStore for CsrfStoreType {
async fn store(&self, key: &str, nonce: &str, redirect_url: Option<String>) -> bool {
match self {
Self::Memory(s) => s.store(key, nonce, redirect_url).await,
}
}
async fn retrieve_and_consume(&self, key: &str) -> Option<(String, Option<String>)> {
match self {
Self::Memory(s) => s.retrieve_and_consume(key).await,
}
}
async fn cleanup_expired(&self) {
match self {
Self::Memory(s) => s.cleanup_expired().await,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[tokio::test]
async fn memory_store_roundtrip() {
let store = MemoryCsrfStore::new();
store
.store("key1", "nonce1", Some("https://app.com".into()))
.await;
let result = store.retrieve_and_consume("key1").await;
assert_eq!(
result,
Some(("nonce1".into(), Some("https://app.com".into())))
);
assert!(store.retrieve_and_consume("key1").await.is_none());
}
#[tokio::test]
async fn memory_store_missing_key() {
let store = MemoryCsrfStore::new();
assert!(store.retrieve_and_consume("nonexistent").await.is_none());
}
#[tokio::test]
async fn memory_store_cleanup() {
let store = MemoryCsrfStore::with_ttl(Duration::from_millis(1));
store.store("key1", "nonce1", None).await;
tokio::time::sleep(Duration::from_millis(5)).await;
store.cleanup_expired().await;
assert!(store.retrieve_and_consume("key1").await.is_none());
}
#[tokio::test]
async fn csrf_store_type_delegates() {
let store = CsrfStoreType::new();
store.store("key1", "nonce1", None).await;
assert!(store.retrieve_and_consume("key1").await.is_some());
}
}