Skip to content

Commit beff652

Browse files
authored
Periodically read workload identity token from file (#1997)
* Periodically read workload identity token from file Fixes #1739 similar to other languages by reading the file every 10 minutes pointed to by AZURE_FEDERATED_TOKEN_FILE. * Resolve PR feedback
1 parent f5a7d2e commit beff652

File tree

2 files changed

+85
-17
lines changed

2 files changed

+85
-17
lines changed

eng/dict/rust-custom.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ bindgen
22
cfg
33
cfgs
44
newtype
5+
oneshot
56
repr
67
rustc
78
rustflags

sdk/identity/azure_identity/src/credentials/workload_identity_credentials.rs

Lines changed: 84 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,19 @@
22
// Licensed under the MIT License.
33

44
use crate::{credentials::cache::TokenCache, federated_credentials_flow, TokenCredentialOptions};
5+
use async_lock::{RwLock, RwLockUpgradableReadGuard};
56
use azure_core::{
67
credentials::{AccessToken, Secret, TokenCredential},
78
error::{ErrorKind, ResultExt},
89
Error, HttpClient, Url,
910
};
10-
use std::{str, sync::Arc, time::Duration};
11+
use futures::channel::oneshot;
12+
use std::{
13+
fs, str,
14+
sync::Arc,
15+
thread,
16+
time::{Duration, Instant},
17+
};
1118
use time::OffsetDateTime;
1219

1320
const AZURE_TENANT_ID_ENV_KEY: &str = "AZURE_TENANT_ID";
@@ -19,14 +26,13 @@ const AZURE_FEDERATED_TOKEN: &str = "AZURE_FEDERATED_TOKEN";
1926
///
2027
/// More information on how to configure a client secret can be found here:
2128
/// <https://learn.microsoft.com/azure/active-directory/develop/quickstart-configure-app-access-web-apis#add-credentials-to-your-web-application>
22-
2329
#[derive(Debug)]
2430
pub struct WorkloadIdentityCredential {
2531
http_client: Arc<dyn HttpClient>,
2632
authority_host: Url,
2733
tenant_id: String,
2834
client_id: String,
29-
token: Secret,
35+
token: Token,
3036
cache: TokenCache,
3137
}
3238

@@ -47,7 +53,7 @@ impl WorkloadIdentityCredential {
4753
authority_host,
4854
tenant_id,
4955
client_id,
50-
token: token.into(),
56+
token: Token::Value(token.into()),
5157
cache: TokenCache::new(),
5258
}))
5359
}
@@ -100,22 +106,14 @@ impl WorkloadIdentityCredential {
100106
.var(AZURE_FEDERATED_TOKEN_FILE)
101107
.map_kind(ErrorKind::Credential)
102108
{
103-
let token = std::fs::read_to_string(token_file.clone()).with_context(
104-
ErrorKind::Credential,
105-
|| {
106-
format!(
107-
"failed to read federated token from file {}",
108-
token_file.as_str()
109-
)
110-
},
111-
)?;
112-
return WorkloadIdentityCredential::new(
109+
return Ok(Arc::new(Self {
113110
http_client,
114111
authority_host,
115112
tenant_id,
116113
client_id,
117-
token,
118-
);
114+
token: Token::with_file(token_file.as_ref())?,
115+
cache: TokenCache::new(),
116+
}));
119117
}
120118

121119
Err(Error::with_message(ErrorKind::Credential, || {
@@ -124,10 +122,11 @@ impl WorkloadIdentityCredential {
124122
}
125123

126124
async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
125+
let token = self.token.secret().await?;
127126
let res: AccessToken = federated_credentials_flow::authorize(
128127
self.http_client.clone(),
129128
&self.client_id,
130-
self.token.secret(),
129+
&token,
131130
scopes,
132131
&self.tenant_id,
133132
&self.authority_host,
@@ -155,3 +154,71 @@ impl TokenCredential for WorkloadIdentityCredential {
155154
self.cache.clear().await
156155
}
157156
}
157+
158+
#[derive(Debug)]
159+
enum Token {
160+
Value(Secret),
161+
File {
162+
path: String,
163+
cache: Arc<RwLock<FileCache>>,
164+
},
165+
}
166+
167+
#[derive(Debug)]
168+
struct FileCache {
169+
token: Secret,
170+
last_read: Instant,
171+
}
172+
173+
impl Token {
174+
fn with_file(path: &str) -> azure_core::Result<Self> {
175+
let last_read = Instant::now();
176+
let token = std::fs::read_to_string(path).with_context(ErrorKind::Credential, || {
177+
format!("failed to read federated token from file {}", path)
178+
})?;
179+
180+
Ok(Self::File {
181+
path: path.into(),
182+
cache: Arc::new(RwLock::new(FileCache {
183+
token: Secret::new(token),
184+
last_read,
185+
})),
186+
})
187+
}
188+
189+
async fn secret(&self) -> azure_core::Result<String> {
190+
match self {
191+
Self::Value(secret) => Ok(secret.secret().into()),
192+
Self::File { path, cache } => {
193+
const TIMEOUT: Duration = Duration::from_secs(600);
194+
195+
let now = Instant::now();
196+
let cache = cache.upgradable_read().await;
197+
if now - cache.last_read > TIMEOUT {
198+
// TODO: https://github.com/Azure/azure-sdk-for-rust/issues/2002
199+
let path = path.clone();
200+
let (tx, rx) = oneshot::channel();
201+
thread::spawn(move || {
202+
let token = fs::read_to_string(&path)
203+
.with_context(ErrorKind::Credential, || {
204+
format!("failed to read federated token from file {}", &path)
205+
});
206+
tx.send(token)
207+
});
208+
209+
let mut write_cache = RwLockUpgradableReadGuard::upgrade(cache).await;
210+
let token = rx.await.map_err(|err| {
211+
azure_core::Error::full(ErrorKind::Io, err, "canceled reading certificate")
212+
})??;
213+
214+
write_cache.token = Secret::new(token);
215+
write_cache.last_read = now;
216+
217+
return Ok(write_cache.token.secret().into());
218+
}
219+
220+
Ok(cache.token.secret().into())
221+
}
222+
}
223+
}
224+
}

0 commit comments

Comments
 (0)