azure_identity/token_credentials/
client_secret_credentials.rs

1use crate::token_credentials::cache::TokenCache;
2use crate::{oauth2_http_client::Oauth2HttpClient, TokenCredentialOptions};
3use azure_core::Error;
4use azure_core::{
5    auth::{AccessToken, Secret, TokenCredential},
6    error::{ErrorKind, ResultExt},
7    HttpClient, Url,
8};
9use oauth2::{basic::BasicClient, AuthType, AuthUrl, Scope, TokenUrl};
10use std::{str, sync::Arc};
11use time::OffsetDateTime;
12
13const AZURE_TENANT_ID_ENV_KEY: &str = "AZURE_TENANT_ID";
14const AZURE_CLIENT_ID_ENV_KEY: &str = "AZURE_CLIENT_ID";
15const AZURE_CLIENT_SECRET_ENV_KEY: &str = "AZURE_CLIENT_SECRET";
16
17/// A list of tenant IDs
18pub mod tenant_ids {
19    /// The tenant ID for multi-tenant apps
20    ///
21    /// <https://docs.microsoft.com/azure/active-directory/develop/howto-convert-app-to-be-multi-tenant>
22    pub const TENANT_ID_COMMON: &str = "common";
23    /// The tenant ID for Active Directory Federated Services
24    pub const TENANT_ID_ADFS: &str = "adfs";
25}
26
27/// Enables authentication to Azure Active Directory using a client secret that was generated for an App Registration.
28///
29/// More information on how to configure a client secret can be found here:
30/// <https://docs.microsoft.com/azure/active-directory/develop/quickstart-configure-app-access-web-apis#add-credentials-to-your-web-application>
31#[derive(Debug)]
32pub struct ClientSecretCredential {
33    http_client: Arc<dyn HttpClient>,
34    authority_host: Url,
35    tenant_id: String,
36    client_id: oauth2::ClientId,
37    client_secret: Option<oauth2::ClientSecret>,
38    cache: TokenCache,
39}
40
41impl ClientSecretCredential {
42    /// Create a new `ClientSecretCredential`
43    pub fn new(
44        http_client: Arc<dyn HttpClient>,
45        authority_host: Url,
46        tenant_id: String,
47        client_id: String,
48        client_secret: String,
49    ) -> ClientSecretCredential {
50        ClientSecretCredential {
51            http_client,
52            authority_host,
53            tenant_id,
54            client_id: oauth2::ClientId::new(client_id),
55            client_secret: Some(oauth2::ClientSecret::new(client_secret)),
56            cache: TokenCache::new(),
57        }
58    }
59
60    async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
61        let mut token_url = self.authority_host.clone();
62        token_url
63            .path_segments_mut()
64            .map_err(|_| {
65                Error::with_message(ErrorKind::Credential, || {
66                    format!("invalid authority host {}", self.authority_host)
67                })
68            })?
69            .extend(&[&self.tenant_id, "oauth2", "v2.0", "token"]);
70
71        let mut auth_url = self.authority_host.clone();
72        auth_url
73            .path_segments_mut()
74            .map_err(|_| {
75                Error::with_message(ErrorKind::Credential, || {
76                    format!("invalid authority host {}", self.authority_host)
77                })
78            })?
79            .extend(&[&self.tenant_id, "oauth2", "v2.0", "authorize"]);
80
81        let client = BasicClient::new(
82            self.client_id.clone(),
83            self.client_secret.clone(),
84            AuthUrl::from_url(auth_url),
85            Some(TokenUrl::from_url(token_url)),
86        )
87        .set_auth_type(AuthType::RequestBody);
88
89        let scopes = scopes.iter().map(ToString::to_string).map(Scope::new);
90        let oauth_http_client = Oauth2HttpClient::new(self.http_client.clone());
91        let token_result = client
92            .exchange_client_credentials()
93            .add_scopes(scopes)
94            .request_async(|request| oauth_http_client.request(request))
95            .await
96            .map(|r| {
97                use oauth2::TokenResponse as _;
98                AccessToken::new(
99                    Secret::new(r.access_token().secret().to_owned()),
100                    OffsetDateTime::now_utc() + r.expires_in().unwrap_or_default(),
101                )
102            })
103            .context(ErrorKind::Credential, "request token error")?;
104
105        Ok(token_result)
106    }
107
108    pub fn create(
109        options: impl Into<TokenCredentialOptions>,
110    ) -> azure_core::Result<ClientSecretCredential> {
111        let options = options.into();
112        let http_client = options.http_client();
113        let authority_host = options.authority_host()?;
114        let env = options.env();
115        let tenant_id =
116            env.var(AZURE_TENANT_ID_ENV_KEY)
117                .with_context(ErrorKind::Credential, || {
118                    format!(
119                        "client secret credential requires {} environment variable",
120                        AZURE_TENANT_ID_ENV_KEY
121                    )
122                })?;
123        let client_id =
124            env.var(AZURE_CLIENT_ID_ENV_KEY)
125                .with_context(ErrorKind::Credential, || {
126                    format!(
127                        "client secret credential requires {} environment variable",
128                        AZURE_CLIENT_ID_ENV_KEY
129                    )
130                })?;
131        let client_secret =
132            env.var(AZURE_CLIENT_SECRET_ENV_KEY)
133                .with_context(ErrorKind::Credential, || {
134                    format!(
135                        "client secret credential requires {} environment variable",
136                        AZURE_CLIENT_SECRET_ENV_KEY
137                    )
138                })?;
139
140        Ok(ClientSecretCredential::new(
141            http_client,
142            authority_host,
143            tenant_id,
144            client_id,
145            client_secret,
146        ))
147    }
148}
149
150#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
151#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
152impl TokenCredential for ClientSecretCredential {
153    async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
154        self.cache.get_token(scopes, self.get_token(scopes)).await
155    }
156    /// Clear the credential's cache.
157    async fn clear_cache(&self) -> azure_core::Result<()> {
158        self.cache.clear().await
159    }
160}