azure_identity/token_credentials/
workload_identity_credentials.rs

1use 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/// Enables authentication to Azure Active Directory using a client secret that was generated for an App Registration.
19///
20/// More information on how to configure a client secret can be found here:
21/// <https://docs.microsoft.com/azure/active-directory/develop/quickstart-configure-app-access-web-apis#add-credentials-to-your-web-application>
22
23#[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    /// Create a new `WorkloadIdentityCredential`
35    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}