Skip to main content

volga_oauth_client/
store.rs

1//! Token store abstraction
2//!
3//! [`TokenStore`] lets [`OAuthClient`](crate::OAuthClient) persist and
4//! reuse tokens across requests: [`token`](crate::OAuthClient::token)
5//! reads through it and refreshes expired entries transparently. The key
6//! is chosen by the application - typically a user or session identifier,
7//! combined with the resource when one client serves several audiences.
8//!
9//! [`InMemoryTokenStore`] is the built-in process-local implementation;
10//! anything durable (database, encrypted file, OS keychain) is one trait
11//! impl away. Implementations own their eviction policy.
12
13use std::{
14    collections::HashMap,
15    sync::{Mutex, PoisonError},
16};
17
18use crate::TokenSet;
19
20/// Storage for tokens obtained by [`OAuthClient`](crate::OAuthClient)
21pub trait TokenStore: Send + Sync {
22    /// Returns the tokens stored under `key`
23    fn get(&self, key: &str) -> Option<TokenSet>;
24
25    /// Stores `tokens` under `key`, replacing any previous entry
26    fn put(&self, key: &str, tokens: &TokenSet);
27
28    /// Removes the entry stored under `key`
29    fn remove(&self, key: &str);
30}
31
32/// A process-local [`TokenStore`] backed by a mutex-guarded map
33///
34/// Suitable for CLIs, tests and single-instance services; tokens do not
35/// survive a restart.
36#[derive(Debug, Default)]
37pub struct InMemoryTokenStore {
38    entries: Mutex<HashMap<String, TokenSet>>,
39}
40
41impl InMemoryTokenStore {
42    /// Creates an empty store
43    #[inline]
44    pub fn new() -> Self {
45        Self::default()
46    }
47}
48
49impl TokenStore for InMemoryTokenStore {
50    fn get(&self, key: &str) -> Option<TokenSet> {
51        self.entries
52            .lock()
53            .unwrap_or_else(PoisonError::into_inner)
54            .get(key)
55            .cloned()
56    }
57
58    fn put(&self, key: &str, tokens: &TokenSet) {
59        self.entries
60            .lock()
61            .unwrap_or_else(PoisonError::into_inner)
62            .insert(key.to_owned(), tokens.clone());
63    }
64
65    fn remove(&self, key: &str) {
66        self.entries
67            .lock()
68            .unwrap_or_else(PoisonError::into_inner)
69            .remove(key);
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    fn tokens(access_token: &str) -> TokenSet {
78        TokenSet {
79            access_token: access_token.into(),
80            token_type: "Bearer".into(),
81            refresh_token: None,
82            scope: None,
83            id_token: None,
84            expires_at: None,
85        }
86    }
87
88    #[test]
89    fn it_stores_replaces_and_removes_entries() {
90        let store = InMemoryTokenStore::new();
91        assert!(store.get("alice").is_none());
92
93        store.put("alice", &tokens("a1"));
94        store.put("bob", &tokens("b1"));
95        assert_eq!(store.get("alice").unwrap().access_token, "a1");
96
97        store.put("alice", &tokens("a2"));
98        assert_eq!(store.get("alice").unwrap().access_token, "a2");
99
100        store.remove("alice");
101        assert!(store.get("alice").is_none());
102        assert!(store.get("bob").is_some());
103    }
104}