Skip to main content

tracedb_sdk/core/
oauth_token_provider.rs

1use std::future::Future;
2use std::sync::Mutex;
3use std::time::{Duration, Instant};
4use tokio::sync::Mutex as AsyncMutex;
5
6/// Buffer time in seconds subtracted from token expiration to ensure
7/// we refresh the token before it actually expires.
8const EXPIRATION_BUFFER_SECONDS: u64 = 120; // 2 minutes
9
10/// Default expiry time in seconds used when the OAuth response doesn't include an expires_in value.
11const DEFAULT_EXPIRY_SECONDS: u64 = 3600; // 1 hour fallback
12
13/// Manages OAuth access tokens, including caching and automatic refresh.
14///
15/// This provider implements thread-safe token management with automatic expiration
16/// handling. It uses a double-checked locking pattern to minimize lock contention
17/// while ensuring only one thread fetches a new token at a time.
18///
19/// # Example
20///
21/// ```rust,ignore
22/// use crate::OAuthTokenProvider;
23///
24/// let provider = OAuthTokenProvider::new("client_id".to_string(), "client_secret".to_string());
25///
26/// // Get or fetch a token (sync)
27/// let token = provider.get_or_fetch(|| {
28///     // Your token fetching logic here
29///     // Returns (access_token, expires_in_seconds)
30///     Ok(("token".to_string(), Some(3600)))
31/// })?;
32///
33/// // Get or fetch a token (async)
34/// let token = provider.get_or_fetch_async(|| async {
35///     // Your async token fetching logic here
36///     Ok(("token".to_string(), Some(3600)))
37/// }).await?;
38/// ```
39pub struct OAuthTokenProvider {
40    client_id: String,
41    client_secret: String,
42    inner: Mutex<OAuthTokenProviderInner>,
43    /// Separate mutex to ensure only one thread fetches a new token at a time (sync)
44    fetch_lock: Mutex<()>,
45    /// Async mutex for async token fetching
46    async_fetch_lock: AsyncMutex<()>,
47}
48
49struct OAuthTokenProviderInner {
50    access_token: Option<String>,
51    expires_at: Option<Instant>,
52}
53
54impl OAuthTokenProvider {
55    /// Creates a new OAuthTokenProvider with the given credentials.
56    pub fn new(client_id: String, client_secret: String) -> Self {
57        Self {
58            client_id,
59            client_secret,
60            inner: Mutex::new(OAuthTokenProviderInner {
61                access_token: None,
62                expires_at: None,
63            }),
64            fetch_lock: Mutex::new(()),
65            async_fetch_lock: AsyncMutex::new(()),
66        }
67    }
68
69    /// Returns the client ID.
70    pub fn client_id(&self) -> &str {
71        &self.client_id
72    }
73
74    /// Returns the client secret.
75    pub fn client_secret(&self) -> &str {
76        &self.client_secret
77    }
78
79    /// Sets the cached access token and its expiration time.
80    ///
81    /// The `expires_in` parameter is the number of seconds until the token expires.
82    /// A buffer is applied to refresh before actual expiration.
83    pub fn set_token(&self, access_token: String, expires_in: u64) {
84        let mut inner = self.inner.lock().unwrap();
85        inner.access_token = Some(access_token);
86
87        if expires_in > 0 {
88            // Apply buffer to refresh before actual expiration
89            let effective_expires_in = expires_in.saturating_sub(EXPIRATION_BUFFER_SECONDS);
90            inner.expires_at = Some(Instant::now() + Duration::from_secs(effective_expires_in));
91        } else {
92            // No expiration info, token won't auto-refresh based on time
93            inner.expires_at = None;
94        }
95    }
96
97    /// Returns the cached access token if it's still valid.
98    ///
99    /// Returns `None` if the token is expired or not set.
100    pub fn get_token(&self) -> Option<String> {
101        let inner = self.inner.lock().unwrap();
102
103        if let Some(ref token) = inner.access_token {
104            // Check if token is still valid
105            if let Some(expires_at) = inner.expires_at {
106                if Instant::now() < expires_at {
107                    return Some(token.clone());
108                }
109            } else {
110                // No expiration set, token is always valid
111                return Some(token.clone());
112            }
113        }
114
115        None
116    }
117
118    /// Returns a valid token, fetching a new one if necessary (synchronous version).
119    ///
120    /// The `fetch_func` is called at most once even if multiple threads call `get_or_fetch`
121    /// concurrently when the token is expired. It should return `(access_token, expires_in_seconds)`.
122    ///
123    /// # Arguments
124    ///
125    /// * `fetch_func` - A function that fetches a new token. Returns `Result<(String, u64), E>`
126    ///   where the tuple contains (access_token, expires_in_seconds).
127    ///
128    /// # Example
129    ///
130    /// ```rust,ignore
131    /// let token = provider.get_or_fetch(|| {
132    ///     // Call your OAuth endpoint here (sync)
133    ///     let response = auth_client.get_token(&provider.client_id(), &provider.client_secret())?;
134    ///     Ok((response.access_token, response.expires_in.unwrap_or(3600)))
135    /// })?;
136    /// ```
137    pub fn get_or_fetch<F, E>(&self, fetch_func: F) -> Result<String, E>
138    where
139        F: FnOnce() -> Result<(String, u64), E>,
140    {
141        // Fast path: check if we have a valid token
142        if let Some(token) = self.get_token() {
143            return Ok(token);
144        }
145
146        // Slow path: acquire fetch lock to ensure only one thread fetches
147        let _fetch_guard = self.fetch_lock.lock().unwrap();
148
149        // Double-check after acquiring lock (another thread may have fetched)
150        if let Some(token) = self.get_token() {
151            return Ok(token);
152        }
153
154        // Fetch new token
155        let (access_token, expires_in) = fetch_func()?;
156
157        // Use default expiry if not provided
158        let effective_expires_in = if expires_in > 0 {
159            expires_in
160        } else {
161            DEFAULT_EXPIRY_SECONDS
162        };
163
164        self.set_token(access_token.clone(), effective_expires_in);
165        Ok(access_token)
166    }
167
168    /// Returns a valid token, fetching a new one if necessary (async version).
169    ///
170    /// This is the async version of `get_or_fetch` for use with async token fetching.
171    /// The `fetch_func` is called at most once even if multiple tasks call `get_or_fetch_async`
172    /// concurrently when the token is expired.
173    ///
174    /// # Arguments
175    ///
176    /// * `fetch_func` - An async function that fetches a new token. Returns `Result<(String, u64), E>`
177    ///   where the tuple contains (access_token, expires_in_seconds).
178    ///
179    /// # Example
180    ///
181    /// ```rust,ignore
182    /// let token = provider.get_or_fetch_async(|| async {
183    ///     // Call your OAuth endpoint here (async)
184    ///     let response = auth_client.get_token(&provider.client_id(), &provider.client_secret()).await?;
185    ///     Ok((response.access_token, response.expires_in.unwrap_or(3600)))
186    /// }).await?;
187    /// ```
188    pub async fn get_or_fetch_async<F, Fut, E>(&self, fetch_func: F) -> Result<String, E>
189    where
190        F: FnOnce() -> Fut,
191        Fut: Future<Output = Result<(String, u64), E>>,
192    {
193        // Fast path: check if we have a valid token
194        if let Some(token) = self.get_token() {
195            return Ok(token);
196        }
197
198        // Slow path: acquire async fetch lock to ensure only one task fetches
199        let _fetch_guard = self.async_fetch_lock.lock().await;
200
201        // Double-check after acquiring lock (another task may have fetched)
202        if let Some(token) = self.get_token() {
203            return Ok(token);
204        }
205
206        // Fetch new token
207        let (access_token, expires_in) = fetch_func().await?;
208
209        // Use default expiry if not provided
210        let effective_expires_in = if expires_in > 0 {
211            expires_in
212        } else {
213            DEFAULT_EXPIRY_SECONDS
214        };
215
216        self.set_token(access_token.clone(), effective_expires_in);
217        Ok(access_token)
218    }
219
220    /// Returns `true` if the token needs to be refreshed.
221    ///
222    /// This is useful for proactively refreshing tokens before they expire.
223    pub fn needs_refresh(&self) -> bool {
224        let inner = self.inner.lock().unwrap();
225
226        if inner.access_token.is_none() {
227            return true;
228        }
229
230        if let Some(expires_at) = inner.expires_at {
231            if Instant::now() >= expires_at {
232                return true;
233            }
234        }
235
236        false
237    }
238
239    /// Clears the cached token.
240    ///
241    /// This can be used to force a token refresh on the next request.
242    pub fn reset(&self) {
243        let mut inner = self.inner.lock().unwrap();
244        inner.access_token = None;
245        inner.expires_at = None;
246    }
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252    use std::sync::atomic::{AtomicUsize, Ordering};
253    use std::sync::Arc;
254    use std::thread;
255
256    #[test]
257    fn test_new_provider() {
258        let provider =
259            OAuthTokenProvider::new("client_id".to_string(), "client_secret".to_string());
260        assert_eq!(provider.client_id(), "client_id");
261        assert_eq!(provider.client_secret(), "client_secret");
262        assert!(provider.get_token().is_none());
263        assert!(provider.needs_refresh());
264    }
265
266    #[test]
267    fn test_set_and_get_token() {
268        let provider =
269            OAuthTokenProvider::new("client_id".to_string(), "client_secret".to_string());
270
271        provider.set_token("test_token".to_string(), 3600);
272
273        let token = provider.get_token();
274        assert!(token.is_some());
275        assert_eq!(token.unwrap(), "test_token");
276        assert!(!provider.needs_refresh());
277    }
278
279    #[test]
280    fn test_expired_token() {
281        let provider =
282            OAuthTokenProvider::new("client_id".to_string(), "client_secret".to_string());
283
284        // Set token with 0 expiry (will be expired immediately due to buffer)
285        provider.set_token("test_token".to_string(), 1);
286
287        // Token should be expired (1 second - 120 second buffer = expired)
288        assert!(provider.get_token().is_none());
289        assert!(provider.needs_refresh());
290    }
291
292    #[test]
293    fn test_get_or_fetch() {
294        let provider =
295            OAuthTokenProvider::new("client_id".to_string(), "client_secret".to_string());
296
297        let result: Result<String, &str> =
298            provider.get_or_fetch(|| Ok(("fetched_token".to_string(), 3600)));
299
300        assert!(result.is_ok());
301        assert_eq!(result.unwrap(), "fetched_token");
302
303        // Second call should return cached token
304        let result2: Result<String, &str> = provider.get_or_fetch(|| {
305            panic!("Should not be called - token is cached");
306        });
307
308        assert!(result2.is_ok());
309        assert_eq!(result2.unwrap(), "fetched_token");
310    }
311
312    #[test]
313    fn test_reset() {
314        let provider =
315            OAuthTokenProvider::new("client_id".to_string(), "client_secret".to_string());
316
317        provider.set_token("test_token".to_string(), 3600);
318        assert!(provider.get_token().is_some());
319
320        provider.reset();
321        assert!(provider.get_token().is_none());
322        assert!(provider.needs_refresh());
323    }
324
325    #[test]
326    fn test_concurrent_access() {
327        let provider = Arc::new(OAuthTokenProvider::new(
328            "client_id".to_string(),
329            "client_secret".to_string(),
330        ));
331        let fetch_count = Arc::new(AtomicUsize::new(0));
332
333        let mut handles = vec![];
334
335        for _ in 0..10 {
336            let provider_clone = Arc::clone(&provider);
337            let fetch_count_clone = Arc::clone(&fetch_count);
338
339            let handle = thread::spawn(move || {
340                let result: Result<String, &str> = provider_clone.get_or_fetch(|| {
341                    fetch_count_clone.fetch_add(1, Ordering::SeqCst);
342                    // Simulate some work
343                    thread::sleep(Duration::from_millis(10));
344                    Ok(("concurrent_token".to_string(), 3600))
345                });
346
347                assert!(result.is_ok());
348                assert_eq!(result.unwrap(), "concurrent_token");
349            });
350
351            handles.push(handle);
352        }
353
354        for handle in handles {
355            handle.join().unwrap();
356        }
357
358        // Due to double-checked locking, fetch should only be called once
359        // (or at most a few times if threads race before the first fetch completes)
360        let count = fetch_count.load(Ordering::SeqCst);
361        assert!(count >= 1 && count <= 3, "Fetch was called {} times", count);
362    }
363}