greentic_oauth_core/
verifier.rs

1use std::{
2    collections::HashMap,
3    sync::Mutex,
4    time::{Duration, Instant},
5};
6
7/// Generic interface for storing PKCE code verifiers keyed by state.
8pub trait CodeVerifierStore: Send + Sync {
9    fn put(&self, state: String, verifier: String, ttl: Duration);
10    fn take(&self, state: &str) -> Option<String>;
11}
12
13#[derive(Debug)]
14struct Entry {
15    verifier: String,
16    expires_at: Instant,
17}
18
19/// In-memory implementation backed by a mutex protected hash map.
20#[derive(Debug, Default)]
21pub struct InMemoryCodeVerifierStore {
22    entries: Mutex<HashMap<String, Entry>>,
23}
24
25impl InMemoryCodeVerifierStore {
26    pub fn new() -> Self {
27        Self {
28            entries: Mutex::new(HashMap::new()),
29        }
30    }
31
32    fn purge_expired(entries: &mut HashMap<String, Entry>) {
33        let now = Instant::now();
34        entries.retain(|_, entry| entry.expires_at > now);
35    }
36}
37
38impl CodeVerifierStore for InMemoryCodeVerifierStore {
39    fn put(&self, state: String, verifier: String, ttl: Duration) {
40        let expires_at = Instant::now() + ttl;
41        let mut guard = self.entries.lock().expect("verifier store lock");
42        Self::purge_expired(&mut guard);
43        guard.insert(
44            state,
45            Entry {
46                verifier,
47                expires_at,
48            },
49        );
50    }
51
52    fn take(&self, state: &str) -> Option<String> {
53        let mut guard = self.entries.lock().expect("verifier store lock");
54        Self::purge_expired(&mut guard);
55        guard.remove(state).and_then(|entry| {
56            if entry.expires_at > Instant::now() {
57                Some(entry.verifier)
58            } else {
59                None
60            }
61        })
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68    use std::{thread, time::Duration};
69
70    #[test]
71    fn put_and_take_roundtrip() {
72        let store = InMemoryCodeVerifierStore::new();
73        store.put(
74            "state-123".into(),
75            "verifier".into(),
76            Duration::from_secs(5),
77        );
78        assert_eq!(store.take("state-123"), Some("verifier".into()));
79        assert_eq!(store.take("state-123"), None);
80    }
81
82    #[test]
83    fn expired_entries_are_dropped() {
84        let store = InMemoryCodeVerifierStore::new();
85        store.put(
86            "state-exp".into(),
87            "verifier".into(),
88            Duration::from_millis(50),
89        );
90        thread::sleep(Duration::from_millis(70));
91        assert_eq!(store.take("state-exp"), None);
92    }
93}