aws_config/profile/
credentials.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Profile File Based Credential Providers
7//!
8//! Profile file based providers combine two pieces:
9//!
10//! 1. Parsing and resolution of the assume role chain
11//! 2. A user-modifiable hashmap of provider name to provider.
12//!
13//! Profile file based providers first determine the chain of providers that will be used to load
14//! credentials. After determining and validating this chain, a `Vec` of providers will be created.
15//!
16//! Each subsequent provider will provide boostrap providers to the next provider in order to load
17//! the final credentials.
18//!
19//! This module contains two sub modules:
20//! - `repr` which contains an abstract representation of a provider chain and the logic to
21//!   build it from `~/.aws/credentials` and `~/.aws/config`.
22//! - `exec` which contains a chain representation of providers to implement passing bootstrapped credentials
23//!   through a series of providers.
24
25use crate::profile::cell::ErrorTakingOnceCell;
26#[allow(deprecated)]
27use crate::profile::profile_file::ProfileFiles;
28use crate::profile::Profile;
29use crate::profile::ProfileFileLoadError;
30use crate::provider_config::ProviderConfig;
31use aws_credential_types::credential_feature::AwsCredentialFeature;
32use aws_credential_types::{
33    provider::{self, error::CredentialsError, future, ProvideCredentials},
34    Credentials,
35};
36use aws_smithy_types::error::display::DisplayErrorContext;
37use std::borrow::Cow;
38use std::collections::HashMap;
39use std::error::Error;
40use std::fmt::{Display, Formatter};
41use std::sync::Arc;
42use tracing::Instrument;
43
44mod exec;
45pub(crate) mod repr;
46
47/// AWS Profile based credentials provider
48///
49/// This credentials provider will load credentials from `~/.aws/config` and `~/.aws/credentials`.
50/// The locations of these files are configurable via environment variables, see [below](#location-of-profile-files).
51///
52/// Generally, this will be constructed via the default provider chain, however, it can be manually
53/// constructed with the builder:
54/// ```rust,no_run
55/// use aws_config::profile::ProfileFileCredentialsProvider;
56/// let provider = ProfileFileCredentialsProvider::builder().build();
57/// ```
58///
59/// _Note: Profile providers, when called, will load and parse the profile from the file system
60/// only once. Parsed file contents will be cached indefinitely._
61///
62/// This provider supports several different credentials formats:
63/// ### Credentials defined explicitly within the file
64/// ```ini
65/// [default]
66/// aws_access_key_id = 123
67/// aws_secret_access_key = 456
68/// ```
69///
70/// ### Assume Role Credentials loaded from a credential source
71/// ```ini
72/// [default]
73/// role_arn = arn:aws:iam::123456789:role/RoleA
74/// credential_source = Environment
75/// ```
76///
77/// NOTE: Currently only the `Environment` credential source is supported although it is possible to
78/// provide custom sources:
79/// ```no_run
80/// use aws_credential_types::provider::{self, future, ProvideCredentials};
81/// use aws_config::profile::ProfileFileCredentialsProvider;
82/// #[derive(Debug)]
83/// struct MyCustomProvider;
84/// impl MyCustomProvider {
85///     async fn load_credentials(&self) -> provider::Result {
86///         todo!()
87///     }
88/// }
89///
90/// impl ProvideCredentials for MyCustomProvider {
91///   fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials where Self: 'a {
92///         future::ProvideCredentials::new(self.load_credentials())
93///     }
94/// }
95/// # if cfg!(feature = "default-https-client") {
96/// let provider = ProfileFileCredentialsProvider::builder()
97///     .with_custom_provider("Custom", MyCustomProvider)
98///     .build();
99/// }
100/// ```
101///
102/// ### Assume role credentials from a source profile
103/// ```ini
104/// [default]
105/// role_arn = arn:aws:iam::123456789:role/RoleA
106/// source_profile = base
107///
108/// [profile base]
109/// aws_access_key_id = 123
110/// aws_secret_access_key = 456
111/// ```
112///
113/// Other more complex configurations are possible, consult `test-data/assume-role-tests.json`.
114///
115/// ### Credentials loaded from an external process
116/// ```ini
117/// [default]
118/// credential_process = /opt/bin/awscreds-custom --username helen
119/// ```
120///
121/// An external process can be used to provide credentials.
122///
123/// ### Loading Credentials from SSO
124/// ```ini
125/// [default]
126/// sso_start_url = https://example.com/start
127/// sso_region = us-east-2
128/// sso_account_id = 123456789011
129/// sso_role_name = readOnly
130/// region = us-west-2
131/// ```
132///
133/// SSO can also be used as a source profile for assume role chains.
134///
135#[doc = include_str!("location_of_profile_files.md")]
136#[derive(Debug)]
137pub struct ProfileFileCredentialsProvider {
138    config: Arc<Config>,
139    inner_provider: ErrorTakingOnceCell<ChainProvider, CredentialsError>,
140}
141
142#[derive(Debug)]
143struct Config {
144    factory: exec::named::NamedProviderFactory,
145    provider_config: ProviderConfig,
146}
147
148impl ProfileFileCredentialsProvider {
149    /// Builder for this credentials provider
150    pub fn builder() -> Builder {
151        Builder::default()
152    }
153
154    async fn load_credentials(&self) -> provider::Result {
155        // The inner provider needs to be cached across successive calls to load_credentials
156        // since the base providers can potentially have information cached in their instances.
157        // For example, the SsoCredentialsProvider maintains an in-memory expiring token cache.
158        let inner_provider = self
159            .inner_provider
160            .get_or_init(
161                {
162                    let config = self.config.clone();
163                    move || async move {
164                        match build_provider_chain(config.clone()).await {
165                            Ok(chain) => Ok(ChainProvider {
166                                config: config.clone(),
167                                chain: Some(Arc::new(chain)),
168                            }),
169                            Err(err) => match err {
170                                ProfileFileError::NoProfilesDefined
171                                | ProfileFileError::ProfileDidNotContainCredentials { .. } => {
172                                    Ok(ChainProvider {
173                                        config: config.clone(),
174                                        chain: None,
175                                    })
176                                }
177                                _ => Err(CredentialsError::invalid_configuration(format!(
178                                    "ProfileFile provider could not be built: {}",
179                                    &err
180                                ))),
181                            },
182                        }
183                    }
184                },
185                CredentialsError::unhandled(
186                    "profile file credentials provider initialization error already taken",
187                ),
188            )
189            .await?;
190        inner_provider.provide_credentials().await.map(|mut creds| {
191            creds
192                .get_property_mut_or_default::<Vec<AwsCredentialFeature>>()
193                .push(AwsCredentialFeature::CredentialsProfile);
194            creds
195        })
196    }
197}
198
199impl ProvideCredentials for ProfileFileCredentialsProvider {
200    fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a>
201    where
202        Self: 'a,
203    {
204        future::ProvideCredentials::new(self.load_credentials())
205    }
206}
207
208/// An Error building a Credential source from an AWS Profile
209#[derive(Debug)]
210#[non_exhaustive]
211pub enum ProfileFileError {
212    /// The profile was not a valid AWS profile
213    #[non_exhaustive]
214    InvalidProfile(ProfileFileLoadError),
215
216    /// No profiles existed (the profile was empty)
217    #[non_exhaustive]
218    NoProfilesDefined,
219
220    /// The profile did not contain any credential information
221    #[non_exhaustive]
222    ProfileDidNotContainCredentials {
223        /// The name of the profile
224        profile: String,
225    },
226
227    /// The profile contained an infinite loop of `source_profile` references
228    #[non_exhaustive]
229    CredentialLoop {
230        /// Vec of profiles leading to the loop
231        profiles: Vec<String>,
232        /// The next profile that caused the loop
233        next: String,
234    },
235
236    /// The profile was missing a credential source
237    #[non_exhaustive]
238    MissingCredentialSource {
239        /// The name of the profile
240        profile: String,
241        /// Error message
242        message: Cow<'static, str>,
243    },
244    /// The profile contained an invalid credential source
245    #[non_exhaustive]
246    InvalidCredentialSource {
247        /// The name of the profile
248        profile: String,
249        /// Error message
250        message: Cow<'static, str>,
251    },
252    /// The profile referred to a another profile by name that was not defined
253    #[non_exhaustive]
254    MissingProfile {
255        /// The name of the profile
256        profile: String,
257        /// Error message
258        message: Cow<'static, str>,
259    },
260    /// The profile referred to `credential_source` that was not defined
261    #[non_exhaustive]
262    UnknownProvider {
263        /// The name of the provider
264        name: String,
265    },
266
267    /// Feature not enabled
268    #[non_exhaustive]
269    FeatureNotEnabled {
270        /// The feature or comma delimited list of features that must be enabled
271        feature: Cow<'static, str>,
272        /// Additional information about the missing feature
273        message: Option<Cow<'static, str>>,
274    },
275
276    /// Missing sso-session section in config
277    #[non_exhaustive]
278    MissingSsoSession {
279        /// The name of the profile that specified `sso_session`
280        profile: String,
281        /// SSO session name
282        sso_session: String,
283    },
284
285    /// Invalid SSO configuration
286    #[non_exhaustive]
287    InvalidSsoConfig {
288        /// The name of the profile that the error originates in
289        profile: String,
290        /// Error message
291        message: Cow<'static, str>,
292    },
293
294    /// Profile is intended to be used in the token provider chain rather
295    /// than in the credentials chain.
296    #[non_exhaustive]
297    TokenProviderConfig {},
298}
299
300impl ProfileFileError {
301    fn missing_field(profile: &Profile, field: &'static str) -> Self {
302        ProfileFileError::MissingProfile {
303            profile: profile.name().to_string(),
304            message: format!("`{field}` was missing").into(),
305        }
306    }
307}
308
309impl Error for ProfileFileError {
310    fn source(&self) -> Option<&(dyn Error + 'static)> {
311        match self {
312            ProfileFileError::InvalidProfile(err) => Some(err),
313            _ => None,
314        }
315    }
316}
317
318impl Display for ProfileFileError {
319    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
320        match self {
321            ProfileFileError::InvalidProfile(err) => {
322                write!(f, "invalid profile: {err}")
323            }
324            ProfileFileError::CredentialLoop { profiles, next } => write!(
325                f,
326                "profile formed an infinite loop. first we loaded {profiles:?}, \
327            then attempted to reload {next}",
328            ),
329            ProfileFileError::MissingCredentialSource { profile, message } => {
330                write!(f, "missing credential source in `{profile}`: {message}")
331            }
332            ProfileFileError::InvalidCredentialSource { profile, message } => {
333                write!(f, "invalid credential source in `{profile}`: {message}")
334            }
335            ProfileFileError::MissingProfile { profile, message } => {
336                write!(f, "profile `{profile}` was not defined: {message}")
337            }
338            ProfileFileError::UnknownProvider { name } => write!(
339                f,
340                "profile referenced `{name}` provider but that provider is not supported",
341            ),
342            ProfileFileError::NoProfilesDefined => write!(f, "No profiles were defined"),
343            ProfileFileError::ProfileDidNotContainCredentials { profile } => write!(
344                f,
345                "profile `{profile}` did not contain credential information"
346            ),
347            ProfileFileError::FeatureNotEnabled { feature, message } => {
348                let message = message.as_deref().unwrap_or_default();
349                write!(
350                    f,
351                    "This behavior requires following cargo feature(s) enabled: {feature}. {message}",
352                )
353            }
354            ProfileFileError::MissingSsoSession {
355                profile,
356                sso_session,
357            } => {
358                write!(f, "sso-session named `{sso_session}` (referenced by profile `{profile}`) was not found")
359            }
360            ProfileFileError::InvalidSsoConfig { profile, message } => {
361                write!(f, "profile `{profile}` has invalid SSO config: {message}")
362            }
363            ProfileFileError::TokenProviderConfig { .. } => {
364                write!(
365                    f,
366                    "selected profile will resolve an access token instead of credentials \
367                     since it doesn't have `sso_account_id` and `sso_role_name` set. Specify both \
368                     `sso_account_id` and `sso_role_name` to let this profile resolve credentials."
369                )
370            }
371        }
372    }
373}
374
375/// Builder for [`ProfileFileCredentialsProvider`]
376#[derive(Debug, Default)]
377pub struct Builder {
378    provider_config: Option<ProviderConfig>,
379    profile_override: Option<String>,
380    #[allow(deprecated)]
381    profile_files: Option<ProfileFiles>,
382    custom_providers: HashMap<Cow<'static, str>, Arc<dyn ProvideCredentials>>,
383}
384
385impl Builder {
386    /// Override the configuration for the [`ProfileFileCredentialsProvider`]
387    ///
388    /// # Examples
389    ///
390    /// ```no_run
391    /// # async fn test() {
392    /// use aws_config::profile::ProfileFileCredentialsProvider;
393    /// use aws_config::provider_config::ProviderConfig;
394    /// let provider = ProfileFileCredentialsProvider::builder()
395    ///     .configure(&ProviderConfig::with_default_region().await)
396    ///     .build();
397    /// # }
398    /// ```
399    pub fn configure(mut self, provider_config: &ProviderConfig) -> Self {
400        self.provider_config = Some(provider_config.clone());
401        self
402    }
403
404    /// Adds a custom credential source
405    ///
406    /// # Examples
407    ///
408    /// ```no_run
409    /// use aws_credential_types::provider::{self, future, ProvideCredentials};
410    /// use aws_config::profile::ProfileFileCredentialsProvider;
411    /// #[derive(Debug)]
412    /// struct MyCustomProvider;
413    /// impl MyCustomProvider {
414    ///     async fn load_credentials(&self) -> provider::Result {
415    ///         todo!()
416    ///     }
417    /// }
418    ///
419    /// impl ProvideCredentials for MyCustomProvider {
420    ///   fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials where Self: 'a {
421    ///         future::ProvideCredentials::new(self.load_credentials())
422    ///     }
423    /// }
424    ///
425    /// # if cfg!(feature = "rustls") {
426    /// let provider = ProfileFileCredentialsProvider::builder()
427    ///     .with_custom_provider("Custom", MyCustomProvider)
428    ///     .build();
429    /// # }
430    /// ```
431    pub fn with_custom_provider(
432        mut self,
433        name: impl Into<Cow<'static, str>>,
434        provider: impl ProvideCredentials + 'static,
435    ) -> Self {
436        self.custom_providers
437            .insert(name.into(), Arc::new(provider));
438        self
439    }
440
441    /// Override the profile name used by the [`ProfileFileCredentialsProvider`]
442    pub fn profile_name(mut self, profile_name: impl Into<String>) -> Self {
443        self.profile_override = Some(profile_name.into());
444        self
445    }
446
447    /// Set the profile file that should be used by the [`ProfileFileCredentialsProvider`]
448    #[allow(deprecated)]
449    pub fn profile_files(mut self, profile_files: ProfileFiles) -> Self {
450        self.profile_files = Some(profile_files);
451        self
452    }
453
454    /// Builds a [`ProfileFileCredentialsProvider`]
455    pub fn build(self) -> ProfileFileCredentialsProvider {
456        let build_span = tracing::debug_span!("build_profile_file_credentials_provider");
457        let _enter = build_span.enter();
458        let conf = self
459            .provider_config
460            .unwrap_or_default()
461            .with_profile_config(self.profile_files, self.profile_override);
462        let mut named_providers = self.custom_providers.clone();
463        named_providers
464            .entry("Environment".into())
465            .or_insert_with(|| {
466                Arc::new(crate::environment::credentials::EnvironmentVariableCredentialsProvider::new_with_env(
467                    conf.env(),
468                ))
469            });
470
471        named_providers
472            .entry("Ec2InstanceMetadata".into())
473            .or_insert_with(|| {
474                Arc::new(
475                    crate::imds::credentials::ImdsCredentialsProvider::builder()
476                        .configure(&conf)
477                        .build(),
478                )
479            });
480
481        named_providers
482            .entry("EcsContainer".into())
483            .or_insert_with(|| {
484                Arc::new(
485                    crate::ecs::EcsCredentialsProvider::builder()
486                        .configure(&conf)
487                        .build(),
488                )
489            });
490        let factory = exec::named::NamedProviderFactory::new(named_providers);
491
492        ProfileFileCredentialsProvider {
493            config: Arc::new(Config {
494                factory,
495                provider_config: conf,
496            }),
497            inner_provider: ErrorTakingOnceCell::new(),
498        }
499    }
500}
501
502async fn build_provider_chain(
503    config: Arc<Config>,
504) -> Result<exec::ProviderChain, ProfileFileError> {
505    let profile_set = config
506        .provider_config
507        .try_profile()
508        .await
509        .map_err(|parse_err| ProfileFileError::InvalidProfile(parse_err.clone()))?;
510    let repr = repr::resolve_chain(profile_set)?;
511    tracing::info!(chain = ?repr, "constructed abstract provider from config file");
512    exec::ProviderChain::from_repr(&config.provider_config, repr, &config.factory)
513}
514
515#[derive(Debug)]
516struct ChainProvider {
517    config: Arc<Config>,
518    chain: Option<Arc<exec::ProviderChain>>,
519}
520
521impl ChainProvider {
522    async fn provide_credentials(&self) -> Result<Credentials, CredentialsError> {
523        // Can't borrow `self` across an await point, or else we lose `Send` on the returned future
524        let config = self.config.clone();
525        let chain = self.chain.clone();
526
527        if let Some(chain) = chain {
528            let mut creds = match chain
529                .base()
530                .provide_credentials()
531                .instrument(tracing::debug_span!("load_base_credentials"))
532                .await
533            {
534                Ok(creds) => {
535                    tracing::info!(creds = ?creds, "loaded base credentials");
536                    creds
537                }
538                Err(e) => {
539                    tracing::warn!(error = %DisplayErrorContext(&e), "failed to load base credentials");
540                    return Err(CredentialsError::provider_error(e));
541                }
542            };
543
544            // we want to create `SdkConfig` _after_ we have resolved the profile or else
545            // we won't get things like `service_config()` set appropriately.
546            let sdk_config = config.provider_config.client_config();
547            for provider in chain.chain().iter() {
548                let next_creds = provider
549                    .credentials(creds, &sdk_config)
550                    .instrument(tracing::debug_span!("load_assume_role", provider = ?provider))
551                    .await;
552                match next_creds {
553                    Ok(next_creds) => {
554                        tracing::info!(creds = ?next_creds, "loaded assume role credentials");
555                        creds = next_creds
556                    }
557                    Err(e) => {
558                        tracing::warn!(provider = ?provider, "failed to load assume role credentials");
559                        return Err(CredentialsError::provider_error(e));
560                    }
561                }
562            }
563            Ok(creds)
564        } else {
565            Err(CredentialsError::not_loaded_no_source())
566        }
567    }
568}
569
570#[cfg(test)]
571mod test {
572    use crate::profile::credentials::Builder;
573    use aws_credential_types::provider::ProvideCredentials;
574
575    macro_rules! make_test {
576        ($name: ident) => {
577            #[tokio::test]
578            async fn $name() {
579                let _ = crate::test_case::TestEnvironment::from_dir(
580                    concat!("./test-data/profile-provider/", stringify!($name)),
581                    crate::test_case::test_credentials_provider(|config| async move {
582                        Builder::default()
583                            .configure(&config)
584                            .build()
585                            .provide_credentials()
586                            .await
587                    }),
588                )
589                .await
590                .unwrap()
591                .execute()
592                .await;
593            }
594        };
595    }
596
597    make_test!(e2e_assume_role);
598    make_test!(e2e_fips_and_dual_stack_sts);
599    make_test!(empty_config);
600    make_test!(retry_on_error);
601    make_test!(invalid_config);
602    make_test!(region_override);
603    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is disabled on Windows because it uses Unix-style paths
604    #[cfg(all(feature = "credentials-process", not(windows)))]
605    make_test!(credential_process);
606    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is disabled on Windows because it uses Unix-style paths
607    #[cfg(all(feature = "credentials-process", not(windows)))]
608    make_test!(credential_process_failure);
609    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is disabled on Windows because it uses Unix-style paths
610    #[cfg(all(feature = "credentials-process", not(windows)))]
611    make_test!(credential_process_account_id_fallback);
612    #[cfg(feature = "credentials-process")]
613    make_test!(credential_process_invalid);
614    #[cfg(feature = "sso")]
615    make_test!(sso_credentials);
616    #[cfg(feature = "sso")]
617    make_test!(invalid_sso_credentials_config);
618    #[cfg(feature = "sso")]
619    make_test!(sso_override_global_env_url);
620    #[cfg(feature = "sso")]
621    make_test!(sso_token);
622
623    make_test!(assume_role_override_global_env_url);
624    make_test!(assume_role_override_service_env_url);
625    make_test!(assume_role_override_global_profile_url);
626    make_test!(assume_role_override_service_profile_url);
627}
628
629#[cfg(all(test, feature = "sso"))]
630mod sso_tests {
631    use crate::{profile::credentials::Builder, provider_config::ProviderConfig};
632    use aws_credential_types::credential_feature::AwsCredentialFeature;
633    use aws_credential_types::provider::ProvideCredentials;
634    use aws_sdk_sso::config::RuntimeComponents;
635    use aws_smithy_runtime_api::client::{
636        http::{
637            HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings,
638            SharedHttpConnector,
639        },
640        orchestrator::{HttpRequest, HttpResponse},
641    };
642    use aws_smithy_types::body::SdkBody;
643    use aws_types::os_shim_internal::{Env, Fs};
644    use std::collections::HashMap;
645
646    #[derive(Debug)]
647    struct ClientInner {
648        expected_token: &'static str,
649    }
650    impl HttpConnector for ClientInner {
651        fn call(&self, request: HttpRequest) -> HttpConnectorFuture {
652            assert_eq!(
653                self.expected_token,
654                request.headers().get("x-amz-sso_bearer_token").unwrap()
655            );
656            HttpConnectorFuture::ready(Ok(HttpResponse::new(
657                    200.try_into().unwrap(),
658                    SdkBody::from("{\"roleCredentials\":{\"accessKeyId\":\"ASIARTESTID\",\"secretAccessKey\":\"TESTSECRETKEY\",\"sessionToken\":\"TESTSESSIONTOKEN\",\"expiration\": 1651516560000}}"),
659                )))
660        }
661    }
662    #[derive(Debug)]
663    struct Client {
664        inner: SharedHttpConnector,
665    }
666    impl Client {
667        fn new(expected_token: &'static str) -> Self {
668            Self {
669                inner: SharedHttpConnector::new(ClientInner { expected_token }),
670            }
671        }
672    }
673    impl HttpClient for Client {
674        fn http_connector(
675            &self,
676            _settings: &HttpConnectorSettings,
677            _components: &RuntimeComponents,
678        ) -> SharedHttpConnector {
679            self.inner.clone()
680        }
681    }
682
683    fn create_test_fs() -> Fs {
684        Fs::from_map({
685            let mut map = HashMap::new();
686            map.insert(
687                "/home/.aws/config".to_string(),
688                br#"
689[profile default]
690sso_session = dev
691sso_account_id = 012345678901
692sso_role_name = SampleRole
693region = us-east-1
694
695[sso-session dev]
696sso_region = us-east-1
697sso_start_url = https://d-abc123.awsapps.com/start
698                "#
699                .to_vec(),
700            );
701            map.insert(
702                "/home/.aws/sso/cache/34c6fceca75e456f25e7e99531e2425c6c1de443.json".to_string(),
703                br#"
704                {
705                    "accessToken": "secret-access-token",
706                    "expiresAt": "2199-11-14T04:05:45Z",
707                    "refreshToken": "secret-refresh-token",
708                    "clientId": "ABCDEFG323242423121312312312312312",
709                    "clientSecret": "ABCDE123",
710                    "registrationExpiresAt": "2199-03-06T19:53:17Z",
711                    "region": "us-east-1",
712                    "startUrl": "https://d-abc123.awsapps.com/start"
713                }
714                "#
715                .to_vec(),
716            );
717            map
718        })
719    }
720
721    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is ignored on Windows because it uses Unix-style paths
722    #[cfg_attr(windows, ignore)]
723    // In order to preserve the SSO token cache, the inner provider must only
724    // be created once, rather than once per credential resolution.
725    #[tokio::test]
726    async fn create_inner_provider_exactly_once() {
727        let fs = create_test_fs();
728
729        let provider_config = ProviderConfig::empty()
730            .with_fs(fs.clone())
731            .with_env(Env::from_slice(&[("HOME", "/home")]))
732            .with_http_client(Client::new("secret-access-token"));
733        let provider = Builder::default().configure(&provider_config).build();
734
735        let first_creds = provider.provide_credentials().await.unwrap();
736
737        // Write to the token cache with an access token that won't match the fake client's
738        // expected access token, and thus, won't return SSO credentials.
739        fs.write(
740            "/home/.aws/sso/cache/34c6fceca75e456f25e7e99531e2425c6c1de443.json",
741            r#"
742            {
743                "accessToken": "NEW!!secret-access-token",
744                "expiresAt": "2199-11-14T04:05:45Z",
745                "refreshToken": "secret-refresh-token",
746                "clientId": "ABCDEFG323242423121312312312312312",
747                "clientSecret": "ABCDE123",
748                "registrationExpiresAt": "2199-03-06T19:53:17Z",
749                "region": "us-east-1",
750                "startUrl": "https://d-abc123.awsapps.com/start"
751            }
752            "#,
753        )
754        .await
755        .unwrap();
756
757        // Loading credentials will still work since the SSOTokenProvider should have only
758        // been created once, and thus, the correct token is still in an in-memory cache.
759        let second_creds = provider
760            .provide_credentials()
761            .await
762            .expect("used cached token instead of loading from the file system");
763        assert_eq!(first_creds, second_creds);
764
765        // Now create a new provider, which should use the new cached token value from the file system
766        // since it won't have the in-memory cache. We do this just to verify that the FS mutation above
767        // actually worked correctly.
768        let provider_config = ProviderConfig::empty()
769            .with_fs(fs.clone())
770            .with_env(Env::from_slice(&[("HOME", "/home")]))
771            .with_http_client(Client::new("NEW!!secret-access-token"));
772        let provider = Builder::default().configure(&provider_config).build();
773        let third_creds = provider.provide_credentials().await.unwrap();
774        assert_eq!(second_creds, third_creds);
775    }
776
777    #[cfg_attr(windows, ignore)]
778    #[tokio::test]
779    async fn credential_feature() {
780        let fs = create_test_fs();
781
782        let provider_config = ProviderConfig::empty()
783            .with_fs(fs.clone())
784            .with_env(Env::from_slice(&[("HOME", "/home")]))
785            .with_http_client(Client::new("secret-access-token"));
786        let provider = Builder::default().configure(&provider_config).build();
787
788        let creds = provider.provide_credentials().await.unwrap();
789
790        assert_eq!(
791            &vec![
792                AwsCredentialFeature::CredentialsSso,
793                AwsCredentialFeature::CredentialsProfile
794            ],
795            creds.get_property::<Vec<AwsCredentialFeature>>().unwrap()
796        )
797    }
798}