azure_identity/token_credentials/
workload_identity_credentials.rs1use crate::{
2 federated_credentials_flow, token_credentials::cache::TokenCache, TokenCredentialOptions,
3};
4use azure_core::{
5 auth::{AccessToken, Secret, TokenCredential},
6 error::{ErrorKind, ResultExt},
7 Error, HttpClient,
8};
9use std::{str, sync::Arc, time::Duration};
10use time::OffsetDateTime;
11use url::Url;
12
13const AZURE_TENANT_ID_ENV_KEY: &str = "AZURE_TENANT_ID";
14const AZURE_CLIENT_ID_ENV_KEY: &str = "AZURE_CLIENT_ID";
15const AZURE_FEDERATED_TOKEN_FILE: &str = "AZURE_FEDERATED_TOKEN_FILE";
16const AZURE_FEDERATED_TOKEN: &str = "AZURE_FEDERATED_TOKEN";
17
18#[derive(Debug)]
24pub struct WorkloadIdentityCredential {
25 http_client: Arc<dyn HttpClient>,
26 authority_host: Url,
27 tenant_id: String,
28 client_id: String,
29 token: Secret,
30 cache: TokenCache,
31}
32
33impl WorkloadIdentityCredential {
34 pub fn new<T>(
36 http_client: Arc<dyn HttpClient>,
37 authority_host: Url,
38 tenant_id: String,
39 client_id: String,
40 token: T,
41 ) -> Self
42 where
43 T: Into<Secret>,
44 {
45 Self {
46 http_client,
47 authority_host,
48 tenant_id,
49 client_id,
50 token: token.into(),
51 cache: TokenCache::new(),
52 }
53 }
54
55 pub fn create(
56 options: impl Into<TokenCredentialOptions>,
57 ) -> azure_core::Result<WorkloadIdentityCredential> {
58 let options = options.into();
59 let http_client = options.http_client();
60 let authority_host = options.authority_host()?;
61 let env = options.env();
62 let tenant_id =
63 env.var(AZURE_TENANT_ID_ENV_KEY)
64 .with_context(ErrorKind::Credential, || {
65 format!(
66 "working identity credential requires {} environment variable",
67 AZURE_TENANT_ID_ENV_KEY
68 )
69 })?;
70 let client_id =
71 env.var(AZURE_CLIENT_ID_ENV_KEY)
72 .with_context(ErrorKind::Credential, || {
73 format!(
74 "working identity credential requires {} environment variable",
75 AZURE_CLIENT_ID_ENV_KEY
76 )
77 })?;
78
79 if let Ok(token) = env
80 .var(AZURE_FEDERATED_TOKEN)
81 .map_kind(ErrorKind::Credential)
82 {
83 return Ok(WorkloadIdentityCredential::new(
84 http_client,
85 authority_host,
86 tenant_id,
87 client_id,
88 token,
89 ));
90 }
91
92 if let Ok(token_file) = env
93 .var(AZURE_FEDERATED_TOKEN_FILE)
94 .map_kind(ErrorKind::Credential)
95 {
96 let token = std::fs::read_to_string(token_file.clone()).with_context(
97 ErrorKind::Credential,
98 || {
99 format!(
100 "failed to read federated token from file {}",
101 token_file.as_str()
102 )
103 },
104 )?;
105 return Ok(WorkloadIdentityCredential::new(
106 http_client,
107 authority_host,
108 tenant_id,
109 client_id,
110 token,
111 ));
112 }
113
114 Err(Error::with_message(ErrorKind::Credential, || {
115 format!("working identity credential requires {AZURE_FEDERATED_TOKEN} or {AZURE_FEDERATED_TOKEN_FILE} environment variables")
116 }))
117 }
118
119 async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
120 let res: AccessToken = federated_credentials_flow::perform(
121 self.http_client.clone(),
122 &self.client_id,
123 self.token.secret(),
124 scopes,
125 &self.tenant_id,
126 &self.authority_host,
127 )
128 .await
129 .map(|r| {
130 AccessToken::new(
131 r.access_token().clone(),
132 OffsetDateTime::now_utc() + Duration::from_secs(r.expires_in),
133 )
134 })
135 .context(ErrorKind::Credential, "request token error")?;
136 Ok(res)
137 }
138}
139
140#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
141#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
142impl TokenCredential for WorkloadIdentityCredential {
143 async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
144 self.cache.get_token(scopes, self.get_token(scopes)).await
145 }
146
147 async fn clear_cache(&self) -> azure_core::Result<()> {
148 self.cache.clear().await
149 }
150}