Skip to main content

github_bot_sdk/auth/
cache.rs

1//! Token caching implementation for GitHub App authentication.
2//!
3//! Provides thread-safe, TTL-based caching for JWT and installation tokens.
4
5use async_trait::async_trait;
6use std::collections::HashMap;
7use std::sync::{Arc, RwLock};
8
9use super::{GitHubAppId, InstallationId, InstallationToken, JsonWebToken, TokenCache};
10use crate::error::CacheError;
11
12/// In-memory token cache with TTL support.
13///
14/// Provides thread-safe caching for both JWT and installation tokens with
15/// automatic expiration handling.
16pub struct InMemoryTokenCache {
17    jwt_cache: Arc<RwLock<HashMap<GitHubAppId, CachedToken<JsonWebToken>>>>,
18    installation_cache: Arc<RwLock<HashMap<InstallationId, CachedToken<InstallationToken>>>>,
19}
20
21/// Cached token with metadata.
22struct CachedToken<T> {
23    token: T,
24}
25
26impl<T> CachedToken<T> {
27    fn new(token: T) -> Self {
28        Self { token }
29    }
30
31    fn token(&self) -> &T {
32        &self.token
33    }
34
35    fn is_valid(&self) -> bool
36    where
37        T: TokenExpiry,
38    {
39        !self.token.is_expired()
40    }
41}
42
43/// Trait for tokens that have expiration.
44trait TokenExpiry {
45    fn is_expired(&self) -> bool;
46}
47
48impl TokenExpiry for JsonWebToken {
49    fn is_expired(&self) -> bool {
50        self.is_expired()
51    }
52}
53
54impl TokenExpiry for InstallationToken {
55    fn is_expired(&self) -> bool {
56        self.is_expired()
57    }
58}
59
60impl InMemoryTokenCache {
61    /// Create a new in-memory token cache.
62    pub fn new() -> Self {
63        Self {
64            jwt_cache: Arc::new(RwLock::new(HashMap::new())),
65            installation_cache: Arc::new(RwLock::new(HashMap::new())),
66        }
67    }
68}
69
70impl Default for InMemoryTokenCache {
71    fn default() -> Self {
72        Self::new()
73    }
74}
75
76#[async_trait]
77impl TokenCache for InMemoryTokenCache {
78    async fn get_jwt(&self, app_id: GitHubAppId) -> Result<Option<JsonWebToken>, CacheError> {
79        let cache = self
80            .jwt_cache
81            .read()
82            .map_err(|e| CacheError::OperationFailed {
83                message: format!("Failed to acquire read lock: {}", e),
84            })?;
85
86        Ok(cache.get(&app_id).map(|cached| cached.token().clone()))
87    }
88
89    async fn store_jwt(&self, jwt: JsonWebToken) -> Result<(), CacheError> {
90        let mut cache = self
91            .jwt_cache
92            .write()
93            .map_err(|e| CacheError::OperationFailed {
94                message: format!("Failed to acquire write lock: {}", e),
95            })?;
96
97        let app_id = jwt.app_id();
98        cache.insert(app_id, CachedToken::new(jwt));
99
100        Ok(())
101    }
102
103    async fn get_installation_token(
104        &self,
105        installation_id: InstallationId,
106    ) -> Result<Option<InstallationToken>, CacheError> {
107        let cache = self
108            .installation_cache
109            .read()
110            .map_err(|e| CacheError::OperationFailed {
111                message: format!("Failed to acquire read lock: {}", e),
112            })?;
113
114        Ok(cache
115            .get(&installation_id)
116            .map(|cached| cached.token().clone()))
117    }
118
119    async fn store_installation_token(&self, token: InstallationToken) -> Result<(), CacheError> {
120        let mut cache =
121            self.installation_cache
122                .write()
123                .map_err(|e| CacheError::OperationFailed {
124                    message: format!("Failed to acquire write lock: {}", e),
125                })?;
126
127        let installation_id = token.installation_id();
128        cache.insert(installation_id, CachedToken::new(token));
129
130        Ok(())
131    }
132
133    async fn invalidate_installation_token(
134        &self,
135        installation_id: InstallationId,
136    ) -> Result<(), CacheError> {
137        let mut cache =
138            self.installation_cache
139                .write()
140                .map_err(|e| CacheError::OperationFailed {
141                    message: format!("Failed to acquire write lock: {}", e),
142                })?;
143
144        cache.remove(&installation_id);
145
146        Ok(())
147    }
148
149    fn cleanup_expired_tokens(&self) {
150        // Cleanup JWT tokens
151        if let Ok(mut jwt_cache) = self.jwt_cache.write() {
152            jwt_cache.retain(|_, cached| cached.is_valid());
153        }
154
155        // Cleanup installation tokens
156        if let Ok(mut inst_cache) = self.installation_cache.write() {
157            inst_cache.retain(|_, cached| cached.is_valid());
158        }
159    }
160}
161
162#[cfg(test)]
163#[path = "cache_tests.rs"]
164mod tests;