gcloud_sdk/token_source/
auth_token_generator.rs1use hyper::header::HeaderValue;
2use jiff::{SignedDuration, Timestamp};
3use tokio::sync::RwLock;
4
5use crate::token_source::*;
6use tracing::*;
7
8struct CachedToken {
11 token: Token,
12 authorization: HeaderValue,
13}
14
15impl CachedToken {
16 fn from_token(token: Token) -> crate::error::Result<Self> {
17 let authorization = token.authorization()?;
18 Ok(Self {
19 token,
20 authorization,
21 })
22 }
23}
24
25pub struct GoogleAuthTokenGenerator {
26 token_source: BoxSource,
27 cached_token: RwLock<Option<CachedToken>>,
28}
29
30impl GoogleAuthTokenGenerator {
31 pub async fn new(
32 token_source_type: TokenSourceType,
33 token_scopes: Vec<String>,
34 ) -> crate::error::Result<GoogleAuthTokenGenerator> {
35 let token_source: BoxSource = create_source(token_source_type, token_scopes).await?;
36 Ok(Self::from_source(token_source))
37 }
38
39 pub fn from_source(token_source: impl Into<BoxSource>) -> GoogleAuthTokenGenerator {
43 GoogleAuthTokenGenerator {
44 token_source: token_source.into(),
45 cached_token: RwLock::new(None),
46 }
47 }
48
49 pub async fn clear_cache(&self) {
50 let mut write_state = self.cached_token.write().await;
51 *write_state = None;
52 }
53
54 pub async fn create_token(&self) -> crate::error::Result<Token> {
55 self.with_cached(|cached| cached.token.clone()).await
56 }
57
58 pub async fn authorization_header(&self) -> crate::error::Result<HeaderValue> {
61 self.with_cached(|cached| cached.authorization.clone())
62 .await
63 }
64
65 async fn with_cached<R>(
68 &self,
69 pick: impl FnOnce(&CachedToken) -> R,
70 ) -> crate::error::Result<R> {
71 let now = Timestamp::now();
72 let refresh_after = now.add(SignedDuration::from_secs(15));
78
79 {
80 let read_state = self.cached_token.read().await;
81 if let Some(cached) = read_state.as_ref() {
82 if cached.token.expiry.gt(&refresh_after) {
83 return Ok(pick(cached));
84 }
85 }
86 }
87
88 let mut write_token = self.cached_token.write().await;
89 match write_token.as_ref() {
90 Some(updated_cached) if updated_cached.token.expiry.gt(&refresh_after) => {
91 Ok(pick(updated_cached))
92 }
93 _ => {
94 let new_token = self.token_source.token().await?;
95 debug!(
96 "Created a new Google OAuth token. Type: {}. Expiring: {}.",
97 new_token.token_type, new_token.expiry,
98 );
99 let new_cached = CachedToken::from_token(new_token)?;
100 let result = pick(&new_cached);
101 *write_token = Some(new_cached);
102 Ok(result)
103 }
104 }
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::token_source::Source;
112 use async_trait::async_trait;
113 use secret_vault_value::SecretValue;
114 use std::sync::atomic::{AtomicUsize, Ordering};
115 use std::sync::Arc;
116
117 struct CountingSource {
118 calls: Arc<AtomicUsize>,
119 }
120
121 #[async_trait]
122 impl Source for CountingSource {
123 async fn token(&self) -> crate::error::Result<Token> {
124 self.calls.fetch_add(1, Ordering::SeqCst);
125 Ok(Token {
126 token_type: "Bearer".to_string(),
127 token: SecretValue::from("cached-token"),
128 expiry: Timestamp::now() + SignedDuration::from_hours(1),
129 })
130 }
131 }
132
133 #[tokio::test]
134 async fn authorization_header_reuses_cached_token() {
135 let calls = Arc::new(AtomicUsize::new(0));
136 let source = CountingSource {
137 calls: calls.clone(),
138 };
139 let generator = GoogleAuthTokenGenerator::new(
140 TokenSourceType::ExternalSource(Box::new(source)),
141 vec![],
142 )
143 .await
144 .unwrap();
145
146 let first = generator.authorization_header().await.unwrap();
147 let second = generator.authorization_header().await.unwrap();
148
149 assert_eq!(first, second);
150 assert!(first.is_sensitive());
151 assert_eq!(calls.load(Ordering::SeqCst), 1);
152 }
153
154 struct FixedExpirySource {
155 calls: Arc<AtomicUsize>,
156 expires_in: SignedDuration,
157 }
158
159 #[async_trait]
160 impl Source for FixedExpirySource {
161 async fn token(&self) -> crate::error::Result<Token> {
162 self.calls.fetch_add(1, Ordering::SeqCst);
163 Ok(Token {
164 token_type: "Bearer".to_string(),
165 token: SecretValue::from("margin-token"),
166 expiry: Timestamp::now() + self.expires_in,
167 })
168 }
169 }
170
171 #[tokio::test]
172 async fn token_inside_refresh_margin_is_refreshed() {
173 let calls = Arc::new(AtomicUsize::new(0));
174 let source = FixedExpirySource {
175 calls: calls.clone(),
176 expires_in: SignedDuration::from_secs(10),
177 };
178 let generator = GoogleAuthTokenGenerator::new(
179 TokenSourceType::ExternalSource(Box::new(source)),
180 vec![],
181 )
182 .await
183 .unwrap();
184
185 generator.authorization_header().await.unwrap();
186 generator.authorization_header().await.unwrap();
187
188 assert_eq!(calls.load(Ordering::SeqCst), 2);
189 }
190}