1use std::time::{Duration, Instant};
4use dashmap::DashMap;
5
6#[async_trait::async_trait]
8pub trait CsrfStore: Send + Sync {
9 async fn store(&self, key: &str, nonce: &str, redirect_url: Option<String>) -> bool;
12
13 async fn retrieve_and_consume(&self, key: &str) -> Option<(String, Option<String>)>;
16
17 async fn cleanup_expired(&self);
19}
20
21pub struct MemoryCsrfStore {
23 entries: DashMap<String, CsrfEntry>,
24 ttl: Duration,
25}
26
27struct CsrfEntry {
28 nonce: String,
29 redirect_url: Option<String>,
30 created_at: Instant,
31}
32
33impl Default for MemoryCsrfStore {
34 fn default() -> Self {
35 Self::new()
36 }
37}
38
39impl MemoryCsrfStore {
40 pub fn new() -> Self {
42 Self {
43 entries: DashMap::new(),
44 ttl: Duration::from_secs(600),
45 }
46 }
47
48 pub fn with_ttl(ttl: Duration) -> Self {
50 Self {
51 entries: DashMap::new(),
52 ttl,
53 }
54 }
55}
56
57#[async_trait::async_trait]
58impl CsrfStore for MemoryCsrfStore {
59 async fn store(&self, key: &str, nonce: &str, redirect_url: Option<String>) -> bool {
60 self.entries.insert(
61 key.to_string(),
62 CsrfEntry {
63 nonce: nonce.to_string(),
64 redirect_url,
65 created_at: Instant::now(),
66 },
67 );
68 true
69 }
70
71 async fn retrieve_and_consume(&self, key: &str) -> Option<(String, Option<String>)> {
72 let entry = self.entries.remove(key)?;
73 if entry.1.created_at.elapsed() > self.ttl {
74 return None;
75 }
76 Some((entry.1.nonce, entry.1.redirect_url))
77 }
78
79 async fn cleanup_expired(&self) {
80 let now = Instant::now();
81 self.entries.retain(|_, entry| now.duration_since(entry.created_at) <= self.ttl);
82 }
83}
84
85pub enum CsrfStoreType {
87 Memory(MemoryCsrfStore),
89}
90
91impl Default for CsrfStoreType {
92 fn default() -> Self {
93 Self::Memory(MemoryCsrfStore::new())
94 }
95}
96
97impl CsrfStoreType {
98 pub fn new() -> Self {
100 Self::Memory(MemoryCsrfStore::new())
101 }
102}
103
104#[async_trait::async_trait]
105impl CsrfStore for CsrfStoreType {
106 async fn store(&self, key: &str, nonce: &str, redirect_url: Option<String>) -> bool {
107 match self {
108 Self::Memory(s) => s.store(key, nonce, redirect_url).await,
109 }
110 }
111
112 async fn retrieve_and_consume(&self, key: &str) -> Option<(String, Option<String>)> {
113 match self {
114 Self::Memory(s) => s.retrieve_and_consume(key).await,
115 }
116 }
117
118 async fn cleanup_expired(&self) {
119 match self {
120 Self::Memory(s) => s.cleanup_expired().await,
121 }
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[tokio::test]
130 async fn memory_store_roundtrip() {
131 let store = MemoryCsrfStore::new();
132 store.store("key1", "nonce1", Some("https://app.com".into())).await;
133 let result = store.retrieve_and_consume("key1").await;
134 assert_eq!(result, Some(("nonce1".into(), Some("https://app.com".into()))));
135 assert!(store.retrieve_and_consume("key1").await.is_none());
136 }
137
138 #[tokio::test]
139 async fn memory_store_missing_key() {
140 let store = MemoryCsrfStore::new();
141 assert!(store.retrieve_and_consume("nonexistent").await.is_none());
142 }
143
144 #[tokio::test]
145 async fn memory_store_cleanup() {
146 let store = MemoryCsrfStore::with_ttl(Duration::from_millis(1));
147 store.store("key1", "nonce1", None).await;
148 tokio::time::sleep(Duration::from_millis(5)).await;
149 store.cleanup_expired().await;
150 assert!(store.retrieve_and_consume("key1").await.is_none());
151 }
152
153 #[tokio::test]
154 async fn csrf_store_type_delegates() {
155 let store = CsrfStoreType::new();
156 store.store("key1", "nonce1", None).await;
157 assert!(store.retrieve_and_consume("key1").await.is_some());
158 }
159}