1use std::fmt;
2use std::sync::Arc;
3use std::time::Duration;
4
5use arc_swap::ArcSwap;
6use async_trait::async_trait;
7use google_cloud_auth::credentials::{AccessTokenCredentials, Builder as AdcCredentialsBuilder};
8use tonic::metadata::MetadataValue;
9
10use crate::error::Error;
11
12const GCS_READ_WRITE_SCOPE: &str = "https://www.googleapis.com/auth/devstorage.read_write";
13const TOKEN_REFRESH_TIMEOUT: Duration = Duration::from_secs(30);
14
15#[derive(Clone, Debug)]
16pub struct RefreshingAuthConfig {
22 pub refresh_interval: Duration,
25 pub retry_interval: Duration,
28}
29
30impl Default for RefreshingAuthConfig {
31 fn default() -> Self {
32 Self {
33 refresh_interval: Duration::from_secs(30),
34 retry_interval: Duration::from_secs(5),
35 }
36 }
37}
38
39#[async_trait]
40pub trait AccessTokenSource: Send + Sync {
45 async fn access_token(&self) -> Result<String, Error>;
47}
48
49#[derive(Clone)]
50pub struct BearerAuth {
56 token: Arc<ArcSwap<String>>,
57 refresh_task: Option<Arc<RefreshTask>>,
58}
59
60impl fmt::Debug for BearerAuth {
61 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
62 formatter
63 .debug_struct("BearerAuth")
64 .field("refreshing", &self.refresh_task.is_some())
65 .finish_non_exhaustive()
66 }
67}
68
69impl BearerAuth {
70 pub fn static_token(token: impl Into<String>) -> Self {
74 Self {
75 token: Arc::new(ArcSwap::from_pointee(token.into())),
76 refresh_task: None,
77 }
78 }
79
80 pub async fn refreshing(
85 source: Arc<dyn AccessTokenSource>,
86 config: RefreshingAuthConfig,
87 ) -> Result<Self, Error> {
88 if config.refresh_interval.is_zero() || config.retry_interval.is_zero() {
89 return Err(Error::InvalidRefreshInterval);
90 }
91 let initial = source.access_token().await?;
92 validate_token(&initial)?;
93 let token = Arc::new(ArcSwap::from_pointee(initial));
94 let task_token = Arc::clone(&token);
95 let task = tokio::spawn(async move {
96 let mut delay = config.refresh_interval;
97 loop {
98 tokio::time::sleep(delay).await;
99 match fetch_refresh_token(source.as_ref(), TOKEN_REFRESH_TIMEOUT).await {
100 Some(Ok(next)) => match validate_token(&next) {
101 Ok(()) => {
102 task_token.store(Arc::new(next));
103 delay = config.refresh_interval;
104 }
105 Err(error) => {
106 tracing::warn!(%error, "auth token refresh returned an invalid token");
107 delay = config.retry_interval;
108 }
109 },
110 Some(Err(error)) => {
111 tracing::warn!(%error, "auth token refresh failed");
112 delay = config.retry_interval;
113 }
114 None => {
115 tracing::warn!(
120 timeout_seconds = TOKEN_REFRESH_TIMEOUT.as_secs(),
121 "auth token refresh timed out"
122 );
123 delay = config.retry_interval;
124 }
125 }
126 }
127 });
128 Ok(Self {
129 token,
130 refresh_task: Some(Arc::new(RefreshTask(task))),
131 })
132 }
133
134 pub async fn google_adc(config: RefreshingAuthConfig) -> Result<Self, Error> {
139 let credentials = AdcCredentialsBuilder::default()
140 .with_scopes([GCS_READ_WRITE_SCOPE])
141 .build_access_token_credentials()
142 .map_err(|error| Error::TokenSource(error.to_string()))?;
143 Self::refreshing(Arc::new(GoogleAdcTokenSource(credentials)), config).await
144 }
145
146 pub(crate) fn authorization_header(
147 &self,
148 ) -> Result<MetadataValue<tonic::metadata::Ascii>, Error> {
149 let token = self.token.load();
150 MetadataValue::try_from(format!("Bearer {}", token.as_str()))
151 .map_err(|_| Error::InvalidToken)
152 }
153}
154
155struct RefreshTask(tokio::task::JoinHandle<()>);
156
157impl Drop for RefreshTask {
158 fn drop(&mut self) {
159 self.0.abort();
160 }
161}
162
163#[derive(Debug)]
164struct GoogleAdcTokenSource(AccessTokenCredentials);
165
166#[async_trait]
167impl AccessTokenSource for GoogleAdcTokenSource {
168 async fn access_token(&self) -> Result<String, Error> {
169 self.0
170 .access_token()
171 .await
172 .map(|token| token.token)
173 .map_err(|error| Error::TokenSource(error.to_string()))
174 }
175}
176
177async fn fetch_refresh_token(
178 source: &dyn AccessTokenSource,
179 timeout: Duration,
180) -> Option<Result<String, Error>> {
181 tokio::time::timeout(timeout, source.access_token())
182 .await
183 .ok()
184}
185
186fn validate_token(token: &str) -> Result<(), Error> {
187 if token.is_empty() {
188 return Err(Error::InvalidToken);
189 }
190 MetadataValue::try_from(format!("Bearer {token}"))
191 .map(|_| ())
192 .map_err(|_| Error::InvalidToken)
193}
194
195#[cfg(test)]
196mod tests {
197 use std::sync::atomic::{AtomicUsize, Ordering};
198
199 use super::*;
200
201 #[derive(Debug)]
202 struct CountingSource(AtomicUsize);
203
204 #[async_trait]
205 impl AccessTokenSource for CountingSource {
206 async fn access_token(&self) -> Result<String, Error> {
207 Ok(format!("token-{}", self.0.fetch_add(1, Ordering::SeqCst)))
208 }
209 }
210
211 #[tokio::test]
212 async fn refresh_updates_existing_auth_handles() {
213 let auth = BearerAuth::refreshing(
214 Arc::new(CountingSource(AtomicUsize::new(0))),
215 RefreshingAuthConfig {
216 refresh_interval: Duration::from_millis(10),
217 retry_interval: Duration::from_millis(5),
218 },
219 )
220 .await
221 .unwrap();
222 let existing_handle = auth.clone();
223 assert_eq!(
224 existing_handle.authorization_header().unwrap(),
225 "Bearer token-0"
226 );
227
228 tokio::time::timeout(Duration::from_secs(1), async {
229 loop {
230 if existing_handle.authorization_header().unwrap() != "Bearer token-0" {
231 break;
232 }
233 tokio::time::sleep(Duration::from_millis(2)).await;
234 }
235 })
236 .await
237 .unwrap();
238 assert_ne!(
239 existing_handle.authorization_header().unwrap(),
240 "Bearer token-0"
241 );
242 }
243
244 #[derive(Debug)]
245 struct HangingSource;
246
247 #[async_trait]
248 impl AccessTokenSource for HangingSource {
249 async fn access_token(&self) -> Result<String, Error> {
250 std::future::pending().await
251 }
252 }
253
254 #[tokio::test]
255 async fn refresh_source_calls_can_be_bounded() {
256 let source = HangingSource;
257 assert!(fetch_refresh_token(&source, Duration::from_millis(10))
258 .await
259 .is_none());
260 }
261}