azure_identity/token_credentials/
default_credentials.rs

1#[cfg(not(target_arch = "wasm32"))]
2use crate::AzureCliCredential;
3use crate::{
4    timeout::TimeoutExt, token_credentials::cache::TokenCache, AppServiceManagedIdentityCredential,
5    EnvironmentCredential, TokenCredentialOptions, VirtualMachineManagedIdentityCredential,
6};
7use azure_core::{
8    auth::{AccessToken, TokenCredential},
9    error::{Error, ErrorKind, ResultExt},
10};
11use std::{sync::Arc, time::Duration};
12
13/// Provides a mechanism of selectively disabling credentials used for a `DefaultAzureCredential` instance
14pub struct DefaultAzureCredentialBuilder {
15    options: TokenCredentialOptions,
16    include_environment_credential: bool,
17    include_app_service_managed_identity_credential: bool,
18    include_virtual_machine_managed_identity_credential: bool,
19    #[cfg(not(target_arch = "wasm32"))]
20    include_azure_cli_credential: bool,
21}
22
23impl Default for DefaultAzureCredentialBuilder {
24    fn default() -> Self {
25        Self {
26            options: TokenCredentialOptions::default(),
27            include_environment_credential: true,
28            include_app_service_managed_identity_credential: true,
29            include_virtual_machine_managed_identity_credential: true,
30            #[cfg(not(target_arch = "wasm32"))]
31            include_azure_cli_credential: true,
32        }
33    }
34}
35
36impl DefaultAzureCredentialBuilder {
37    /// Create a new `DefaultAzureCredentialBuilder`
38    pub fn new() -> Self {
39        Self::default()
40    }
41
42    pub fn with_options(&mut self, options: impl Into<TokenCredentialOptions>) -> &mut Self {
43        self.options = options.into();
44        self
45    }
46
47    /// Exclude using any environment credential
48    pub fn exclude_environment_credential(&mut self) -> &mut Self {
49        self.include_environment_credential = false;
50        self
51    }
52
53    /// Exclude using any managed identity credential
54    pub fn exclude_managed_identity_credential(&mut self) -> &mut Self {
55        self.include_app_service_managed_identity_credential = false;
56        self.include_virtual_machine_managed_identity_credential = false;
57        self
58    }
59
60    /// Exclude using virtual machine managed identity credential
61    pub fn exclude_virtual_machine_managed_identity_credential(&mut self) -> &mut Self {
62        self.include_virtual_machine_managed_identity_credential = false;
63        self
64    }
65
66    /// Include using virtual machine managed identity credential
67    pub fn include_virtual_machine_managed_identity_credential(&mut self) -> &mut Self {
68        self.include_virtual_machine_managed_identity_credential = true;
69        self
70    }
71
72    /// Include using app service managed identity credential
73    pub fn include_app_service_managed_identity_credential(&mut self) -> &mut Self {
74        self.include_app_service_managed_identity_credential = true;
75        self
76    }
77
78    /// Exclude using credential from the cli
79    #[cfg(not(target_arch = "wasm32"))]
80    pub fn exclude_azure_cli_credential(&mut self) -> &mut Self {
81        self.include_azure_cli_credential = false;
82        self
83    }
84
85    /// Get a list of the credential types to include.
86    fn included(&self) -> Vec<DefaultAzureCredentialType> {
87        let mut sources = Vec::new();
88        if self.include_environment_credential {
89            sources.push(DefaultAzureCredentialType::Environment);
90        }
91        if self.include_app_service_managed_identity_credential {
92            sources.push(DefaultAzureCredentialType::AppService);
93        }
94        if self.include_virtual_machine_managed_identity_credential {
95            sources.push(DefaultAzureCredentialType::VirtualMachine);
96        }
97        #[cfg(not(target_arch = "wasm32"))]
98        if self.include_azure_cli_credential {
99            sources.push(DefaultAzureCredentialType::AzureCli);
100        }
101        sources
102    }
103
104    /// Creates a list of `TokenCredential` instances from the included credential types.
105    /// The credentials created successfully are used as sources for getting a token.
106    fn create_sources(
107        &self,
108        included: &Vec<DefaultAzureCredentialType>,
109    ) -> azure_core::Result<Vec<DefaultAzureCredentialKind>> {
110        let mut sources = Vec::<DefaultAzureCredentialKind>::with_capacity(included.len());
111        let mut errors = Vec::new();
112        for source in included {
113            match source {
114                DefaultAzureCredentialType::Environment => {
115                    match EnvironmentCredential::create(self.options.clone()) {
116                        Ok(credential) => {
117                            sources.push(DefaultAzureCredentialKind::Environment(credential))
118                        }
119                        Err(error) => errors.push(error),
120                    }
121                }
122                DefaultAzureCredentialType::AppService => {
123                    match AppServiceManagedIdentityCredential::create(self.options.clone()) {
124                        Ok(credential) => {
125                            sources.push(DefaultAzureCredentialKind::AppService(credential))
126                        }
127                        Err(error) => errors.push(error),
128                    }
129                }
130                DefaultAzureCredentialType::VirtualMachine => {
131                    sources.push(DefaultAzureCredentialKind::VirtualMachine(
132                        VirtualMachineManagedIdentityCredential::new(self.options.clone()),
133                    ));
134                }
135                #[cfg(not(target_arch = "wasm32"))]
136                DefaultAzureCredentialType::AzureCli => {
137                    if let Ok(credential) = AzureCliCredential::create() {
138                        sources.push(DefaultAzureCredentialKind::AzureCli(credential));
139                    }
140                }
141            }
142        }
143        if sources.is_empty() {
144            return Err(Error::with_message(ErrorKind::Credential, || {
145                format!(
146                    "No credential sources were available to be used for authentication.\n{}",
147                    format_aggregate_error(&errors)
148                )
149            }));
150        }
151        Ok(sources)
152    }
153
154    /// Create a `DefaultAzureCredential` from this builder.
155    pub fn build(&self) -> azure_core::Result<DefaultAzureCredential> {
156        let included = self.included();
157        let sources = self.create_sources(&included)?;
158        Ok(DefaultAzureCredential::with_sources(sources))
159    }
160}
161
162/// Types that may be enabled for use by `DefaultAzureCredential`.
163#[derive(Debug, PartialEq)]
164enum DefaultAzureCredentialType {
165    Environment,
166    AppService,
167    VirtualMachine,
168    #[cfg(not(target_arch = "wasm32"))]
169    AzureCli,
170}
171
172/// Types of `TokenCredential` supported by `DefaultAzureCredential`
173#[derive(Debug)]
174pub(crate) enum DefaultAzureCredentialKind {
175    /// `TokenCredential` from environment variable.
176    Environment(EnvironmentCredential),
177    /// `TokenCredential` from managed identity that has been assigned to an App Service.
178    AppService(AppServiceManagedIdentityCredential),
179    /// `TokenCredential` from managed identity that has been assigned to a virtual machine.
180    VirtualMachine(VirtualMachineManagedIdentityCredential),
181    #[cfg(not(target_arch = "wasm32"))]
182    /// `TokenCredential` from Azure CLI.
183    AzureCli(AzureCliCredential),
184}
185
186#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
187#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
188impl TokenCredential for DefaultAzureCredentialKind {
189    async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
190        match self {
191            DefaultAzureCredentialKind::Environment(credential) => {
192                credential.get_token(scopes).await.context(
193                    ErrorKind::Credential,
194                    "error getting environment credential",
195                )
196            }
197            DefaultAzureCredentialKind::AppService(credential) => {
198                credential.get_token(scopes).await.context(
199                    ErrorKind::Credential,
200                    "error getting managed identity credential for App Service",
201                )
202            }
203            DefaultAzureCredentialKind::VirtualMachine(credential) => {
204                // IMSD timeout is only limited to 1 second when used in DefaultAzureCredential
205                credential
206                    .get_token(scopes)
207                    .timeout(Duration::from_secs(1))
208                    .await
209                    .context(
210                        ErrorKind::Credential,
211                        "getting virtual machine managed identity credential timed out",
212                    )?
213                    .context(
214                        ErrorKind::Credential,
215                        "error getting virtual machine managed identity credential",
216                    )
217            }
218            #[cfg(not(target_arch = "wasm32"))]
219            DefaultAzureCredentialKind::AzureCli(credential) => {
220                credential.get_token(scopes).await.context(
221                    ErrorKind::Credential,
222                    "error getting token credential from Azure CLI",
223                )
224            }
225        }
226    }
227
228    /// Clear the credential's cache.
229    async fn clear_cache(&self) -> azure_core::Result<()> {
230        match self {
231            DefaultAzureCredentialKind::Environment(credential) => credential.clear_cache().await,
232            DefaultAzureCredentialKind::AppService(credential) => credential.clear_cache().await,
233            DefaultAzureCredentialKind::VirtualMachine(credential) => {
234                credential.clear_cache().await
235            }
236            #[cfg(not(target_arch = "wasm32"))]
237            DefaultAzureCredentialKind::AzureCli(credential) => credential.clear_cache().await,
238        }
239    }
240}
241
242/// Provides a default `TokenCredential` authentication flow for applications that will be deployed to Azure.
243///
244/// The following credential types if enabled will be tried, in order:
245/// - `EnvironmentCredential`
246/// - `ManagedIdentityCredential`
247/// - `AzureCliCredential`
248///
249/// Consult the documentation of these credential types for more information on how they attempt authentication.
250#[derive(Debug)]
251pub struct DefaultAzureCredential {
252    sources: Vec<DefaultAzureCredentialKind>,
253    cache: TokenCache,
254}
255
256impl DefaultAzureCredential {
257    pub fn create(options: TokenCredentialOptions) -> azure_core::Result<DefaultAzureCredential> {
258        DefaultAzureCredentialBuilder::default()
259            .with_options(options)
260            .build()
261    }
262
263    /// Creates a `DefaultAzureCredential` with specified sources.
264    fn with_sources(sources: Vec<DefaultAzureCredentialKind>) -> Self {
265        DefaultAzureCredential {
266            sources,
267            cache: TokenCache::new(),
268        }
269    }
270
271    /// Try to fetch a token using each of the credential sources until one succeeds
272    async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
273        let mut errors = Vec::new();
274        for source in &self.sources {
275            let token_res = source.get_token(scopes).await;
276
277            match token_res {
278                Ok(token) => return Ok(token),
279                Err(error) => errors.push(error),
280            }
281        }
282        Err(Error::with_message(ErrorKind::Credential, || {
283            format!(
284                "Multiple errors were encountered while attempting to authenticate:\n{}",
285                format_aggregate_error(&errors)
286            )
287        }))
288    }
289}
290
291/// Creates a new `DefaultAzureCredential` with the default options.
292pub fn create_default_credential() -> azure_core::Result<Arc<dyn TokenCredential>> {
293    DefaultAzureCredentialBuilder::default()
294        .build()
295        .map(|cred| Arc::new(cred) as Arc<dyn TokenCredential>)
296}
297
298#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
299#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
300impl TokenCredential for DefaultAzureCredential {
301    async fn get_token(&self, scopes: &[&str]) -> azure_core::Result<AccessToken> {
302        self.cache.get_token(scopes, self.get_token(scopes)).await
303    }
304
305    /// Clear the credential's cache.
306    async fn clear_cache(&self) -> azure_core::Result<()> {
307        // clear the internal cache as well as each of the underlying providers
308        self.cache.clear().await?;
309
310        for source in &self.sources {
311            source.clear_cache().await?;
312        }
313
314        Ok(())
315    }
316}
317
318fn format_aggregate_error(errors: &[Error]) -> String {
319    use std::error::Error;
320    errors
321        .iter()
322        .map(|e| {
323            let mut current: Option<&dyn Error> = Some(e);
324            let mut stack = vec![];
325            while let Some(err) = current.take() {
326                stack.push(err.to_string());
327                current = err.source();
328            }
329            stack.join(" - ")
330        })
331        .collect::<Vec<String>>()
332        .join("\n")
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338
339    #[test]
340    fn test_builder_included_credential_flags() {
341        let builder = DefaultAzureCredentialBuilder::new();
342        #[cfg(not(target_arch = "wasm32"))]
343        assert!(builder.include_azure_cli_credential);
344        assert!(builder.include_environment_credential);
345        assert!(builder.include_app_service_managed_identity_credential);
346        assert!(builder.include_virtual_machine_managed_identity_credential);
347
348        #[cfg(not(target_arch = "wasm32"))]
349        {
350            let mut builder = DefaultAzureCredentialBuilder::new();
351            builder.exclude_azure_cli_credential();
352            assert!(!builder.include_azure_cli_credential);
353            assert!(builder.include_environment_credential);
354            assert!(builder.include_app_service_managed_identity_credential);
355            assert!(builder.include_virtual_machine_managed_identity_credential);
356        }
357
358        let mut builder = DefaultAzureCredentialBuilder::new();
359        builder.exclude_environment_credential();
360        #[cfg(not(target_arch = "wasm32"))]
361        assert!(builder.include_azure_cli_credential);
362        assert!(!builder.include_environment_credential);
363        assert!(builder.include_app_service_managed_identity_credential);
364        assert!(builder.include_virtual_machine_managed_identity_credential);
365
366        let mut builder = DefaultAzureCredentialBuilder::new();
367        builder.exclude_managed_identity_credential();
368        #[cfg(not(target_arch = "wasm32"))]
369        assert!(builder.include_azure_cli_credential);
370        assert!(builder.include_environment_credential);
371        assert!(!builder.include_app_service_managed_identity_credential);
372        assert!(!builder.include_virtual_machine_managed_identity_credential);
373    }
374
375    #[test]
376    /// test default included credential types
377    fn test_default_included_credential_types() {
378        let builder = DefaultAzureCredentialBuilder::new();
379        assert_eq!(
380            builder.included(),
381            vec![
382                DefaultAzureCredentialType::Environment,
383                DefaultAzureCredentialType::AppService,
384                DefaultAzureCredentialType::VirtualMachine,
385                DefaultAzureCredentialType::AzureCli,
386            ]
387        );
388    }
389
390    /// test excluding virtual machine managed identity credential
391    #[test]
392    fn test_exclude_virtual_machine_managed_identity_credential() {
393        let mut builder = DefaultAzureCredentialBuilder::new();
394        builder.exclude_virtual_machine_managed_identity_credential();
395        assert_eq!(
396            builder.included(),
397            vec![
398                DefaultAzureCredentialType::Environment,
399                DefaultAzureCredentialType::AppService,
400                DefaultAzureCredentialType::AzureCli,
401            ]
402        );
403    }
404
405    /// test excluding environment credential
406    #[test]
407    fn test_exclude_environment_credential() -> azure_core::Result<()> {
408        let mut builder = DefaultAzureCredentialBuilder::new();
409        builder.exclude_environment_credential();
410        assert_eq!(
411            builder.included(),
412            vec![
413                DefaultAzureCredentialType::AppService,
414                DefaultAzureCredentialType::VirtualMachine,
415                DefaultAzureCredentialType::AzureCli,
416            ]
417        );
418        Ok(())
419    }
420
421    /// test excluding azure cli credential
422    #[test]
423    fn test_exclude_azure_cli_credential() {
424        let mut builder = DefaultAzureCredentialBuilder::new();
425        builder.exclude_azure_cli_credential();
426        assert_eq!(
427            builder.included(),
428            vec![
429                DefaultAzureCredentialType::Environment,
430                DefaultAzureCredentialType::AppService,
431                DefaultAzureCredentialType::VirtualMachine,
432            ]
433        );
434    }
435
436    /// test exluding managed identity credential
437    #[test]
438    fn test_exclude_managed_identity_credential() {
439        let mut builder = DefaultAzureCredentialBuilder::new();
440        builder.exclude_managed_identity_credential();
441        assert_eq!(
442            builder.included(),
443            vec![
444                DefaultAzureCredentialType::Environment,
445                DefaultAzureCredentialType::AzureCli,
446            ]
447        );
448    }
449}