spiral_auth/providers/
service.rs1use std::fmt::{Debug, Formatter};
2use std::sync::Arc;
3use std::time::Duration;
4
5use tokio::sync::{Mutex, RwLock};
6use tokio::task::JoinHandle;
7
8use crate::{CredentialResult, Token, TokenSource};
9
10const DEFAULT_REFRESH_BEFORE: Duration = Duration::from_secs(60);
11const DEFAULT_RETRY_DELAY: Duration = Duration::from_secs(5);
12const MIN_USABLE_LIFETIME: Duration = Duration::from_secs(30);
13
14struct ServiceTokenState {
15 source: TokenSource,
16 cached: RwLock<Option<Token>>,
17 refresh: Mutex<()>,
18 refresh_before: Duration,
19 retry_delay: Duration,
20}
21
22pub struct RefreshingAuthTokenProvider {
29 state: Arc<ServiceTokenState>,
30 background: JoinHandle<()>,
31}
32
33impl RefreshingAuthTokenProvider {
34 pub fn new(source: TokenSource) -> Self {
36 Self::with_timing(source, DEFAULT_REFRESH_BEFORE, DEFAULT_RETRY_DELAY)
37 }
38
39 fn with_timing(source: TokenSource, refresh_before: Duration, retry_delay: Duration) -> Self {
40 let state = Arc::new(ServiceTokenState {
41 source,
42 cached: RwLock::new(None),
43 refresh: Mutex::new(()),
44 refresh_before,
45 retry_delay,
46 });
47 let background_state = Arc::clone(&state);
48 let background = tokio::spawn(async move {
49 background_state.refresh_loop().await;
50 });
51 Self { state, background }
52 }
53}
54
55impl Debug for RefreshingAuthTokenProvider {
56 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
57 f.debug_struct("RefreshingAuthTokenProvider")
58 .field("source", &self.state.source)
59 .finish_non_exhaustive()
60 }
61}
62
63impl Drop for RefreshingAuthTokenProvider {
64 fn drop(&mut self) {
65 self.background.abort();
66 }
67}
68
69impl RefreshingAuthTokenProvider {
70 pub async fn token(&self) -> CredentialResult<Option<Token>> {
72 self.state
73 .token_with_lifetime(MIN_USABLE_LIFETIME)
74 .await
75 .map(Some)
76 }
77
78 pub async fn cached_token(&self) -> CredentialResult<Option<Token>> {
80 self.state.cached_with_lifetime(MIN_USABLE_LIFETIME).await
81 }
82}
83
84impl_token_source!(RefreshingAuthTokenProvider, cached);
85
86impl ServiceTokenState {
87 async fn refresh_loop(self: Arc<Self>) {
88 loop {
89 if let Err(error) = self.token_with_lifetime(self.refresh_before).await {
90 tracing::warn!(?error, "failed to refresh service token");
91 }
92
93 tokio::time::sleep(self.next_refresh_delay().await).await;
94 }
95 }
96
97 async fn next_refresh_delay(&self) -> Duration {
98 let cached = self.cached.read().await;
99 let Some(token) = cached.as_ref() else {
100 return self.retry_delay;
101 };
102 match token.remaining_lifetime() {
103 Ok(remaining) if remaining > self.refresh_before => remaining - self.refresh_before,
104 _ => self.retry_delay,
105 }
106 }
107
108 async fn token_with_lifetime(&self, minimum_lifetime: Duration) -> CredentialResult<Token> {
109 if let Some(token) = self.cached_with_lifetime(minimum_lifetime).await? {
110 return Ok(token);
111 }
112
113 let _refresh = self.refresh.lock().await;
114 if let Some(token) = self.cached_with_lifetime(minimum_lifetime).await? {
115 return Ok(token);
116 }
117
118 let Some(token) = self.source.token().await? else {
119 bail!("service token source returned no token");
120 };
121 let remaining = token
122 .remaining_lifetime()
123 .map_err(|error| invalid!("service token is not a valid JWT: {error}"))?;
124 if remaining < MIN_USABLE_LIFETIME {
125 bail!("service token expires too soon");
126 }
127 *self.cached.write().await = Some(token.clone());
128 Ok(token)
129 }
130
131 async fn cached_with_lifetime(
132 &self,
133 minimum_lifetime: Duration,
134 ) -> CredentialResult<Option<Token>> {
135 let cached = self.cached.read().await;
136 let Some(token) = cached.as_ref() else {
137 return Ok(None);
138 };
139 let remaining = token
140 .remaining_lifetime()
141 .map_err(|error| invalid!("cached service token is not a valid JWT: {error}"))?;
142 Ok((remaining >= minimum_lifetime).then(|| token.clone()))
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use std::collections::VecDeque;
149 use std::sync::atomic::{AtomicUsize, Ordering};
150
151 use serde_json::json;
152
153 use super::*;
154 use crate::token::unsigned_jwt;
155
156 fn token(expires_in: Duration) -> Token {
157 let now = std::time::SystemTime::now()
158 .duration_since(std::time::SystemTime::UNIX_EPOCH)
159 .unwrap()
160 .as_secs();
161 unsigned_jwt(json!({ "exp": now + expires_in.as_secs() }))
162 }
163
164 struct SequenceProvider {
165 tokens: Mutex<VecDeque<Token>>,
166 calls: AtomicUsize,
167 }
168
169 impl Debug for SequenceProvider {
170 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
171 f.debug_struct("SequenceProvider").finish_non_exhaustive()
172 }
173 }
174
175 impl SequenceProvider {
176 pub async fn token(&self) -> CredentialResult<Option<Token>> {
178 self.calls.fetch_add(1, Ordering::SeqCst);
179 let mut tokens = self.tokens.lock().await;
180 Ok(tokens.pop_front().or_else(|| tokens.back().cloned()))
181 }
182 }
183
184 impl_token_source!(Arc<SequenceProvider>);
185
186 fn source(tokens: impl IntoIterator<Item = Token>) -> Arc<SequenceProvider> {
187 Arc::new(SequenceProvider {
188 tokens: Mutex::new(tokens.into_iter().collect()),
189 calls: AtomicUsize::new(0),
190 })
191 }
192
193 #[tokio::test]
194 async fn concurrent_getters_share_one_refresh() {
195 let source = source([token(Duration::from_secs(3_600))]);
196 let provider = TokenSource::from(RefreshingAuthTokenProvider::new(source.clone().into()));
197 let getters = (0..20).map(|_| {
198 let provider = provider.clone();
199 tokio::spawn(async move { provider.token().await.unwrap().unwrap() })
200 });
201 for getter in getters {
202 getter.await.unwrap();
203 }
204 assert_eq!(source.calls.load(Ordering::SeqCst), 1);
205 }
206
207 #[tokio::test]
208 async fn refreshes_early_in_the_background() {
209 let first = token(Duration::from_secs(90));
210 let second = token(Duration::from_secs(3_600));
211 let source = source([first.clone(), second.clone()]);
212 let provider = RefreshingAuthTokenProvider::with_timing(
213 source.clone().into(),
214 Duration::from_secs(100),
215 Duration::from_millis(5),
216 );
217
218 tokio::time::timeout(Duration::from_secs(1), async {
219 while source.calls.load(Ordering::SeqCst) < 2 {
220 tokio::time::sleep(Duration::from_millis(5)).await;
221 }
222 })
223 .await
224 .unwrap();
225
226 assert_eq!(provider.token().await.unwrap().unwrap(), second);
227 }
228
229 #[tokio::test]
230 async fn rejects_a_token_that_is_already_unusable() {
231 let source = source([token(Duration::from_secs(5))]);
232 let provider = RefreshingAuthTokenProvider::new(source.into());
233 let error = provider.token().await.unwrap_err();
234 assert!(error.to_string().contains("expires too soon"));
235 }
236}