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!("`{}` was missing", field).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 {:?}, \
327            then attempted to reload {}",
328                profiles, next
329            ),
330            ProfileFileError::MissingCredentialSource { profile, message } => {
331                write!(f, "missing credential source in `{}`: {}", profile, message)
332            }
333            ProfileFileError::InvalidCredentialSource { profile, message } => {
334                write!(f, "invalid credential source in `{}`: {}", profile, message)
335            }
336            ProfileFileError::MissingProfile { profile, message } => {
337                write!(f, "profile `{}` was not defined: {}", profile, message)
338            }
339            ProfileFileError::UnknownProvider { name } => write!(
340                f,
341                "profile referenced `{}` provider but that provider is not supported",
342                name
343            ),
344            ProfileFileError::NoProfilesDefined => write!(f, "No profiles were defined"),
345            ProfileFileError::ProfileDidNotContainCredentials { profile } => write!(
346                f,
347                "profile `{}` did not contain credential information",
348                profile
349            ),
350            ProfileFileError::FeatureNotEnabled { feature, message } => {
351                let message = message.as_deref().unwrap_or_default();
352                write!(
353                    f,
354                    "This behavior requires following cargo feature(s) enabled: {feature}. {message}",
355                )
356            }
357            ProfileFileError::MissingSsoSession {
358                profile,
359                sso_session,
360            } => {
361                write!(f, "sso-session named `{sso_session}` (referenced by profile `{profile}`) was not found")
362            }
363            ProfileFileError::InvalidSsoConfig { profile, message } => {
364                write!(f, "profile `{profile}` has invalid SSO config: {message}")
365            }
366            ProfileFileError::TokenProviderConfig { .. } => {
367                write!(
368                    f,
369                    "selected profile will resolve an access token instead of credentials \
370                     since it doesn't have `sso_account_id` and `sso_role_name` set. Specify both \
371                     `sso_account_id` and `sso_role_name` to let this profile resolve credentials."
372                )
373            }
374        }
375    }
376}
377
378/// Builder for [`ProfileFileCredentialsProvider`]
379#[derive(Debug, Default)]
380pub struct Builder {
381    provider_config: Option<ProviderConfig>,
382    profile_override: Option<String>,
383    #[allow(deprecated)]
384    profile_files: Option<ProfileFiles>,
385    custom_providers: HashMap<Cow<'static, str>, Arc<dyn ProvideCredentials>>,
386}
387
388impl Builder {
389    /// Override the configuration for the [`ProfileFileCredentialsProvider`]
390    ///
391    /// # Examples
392    ///
393    /// ```no_run
394    /// # async fn test() {
395    /// use aws_config::profile::ProfileFileCredentialsProvider;
396    /// use aws_config::provider_config::ProviderConfig;
397    /// let provider = ProfileFileCredentialsProvider::builder()
398    ///     .configure(&ProviderConfig::with_default_region().await)
399    ///     .build();
400    /// # }
401    /// ```
402    pub fn configure(mut self, provider_config: &ProviderConfig) -> Self {
403        self.provider_config = Some(provider_config.clone());
404        self
405    }
406
407    /// Adds a custom credential source
408    ///
409    /// # Examples
410    ///
411    /// ```no_run
412    /// use aws_credential_types::provider::{self, future, ProvideCredentials};
413    /// use aws_config::profile::ProfileFileCredentialsProvider;
414    /// #[derive(Debug)]
415    /// struct MyCustomProvider;
416    /// impl MyCustomProvider {
417    ///     async fn load_credentials(&self) -> provider::Result {
418    ///         todo!()
419    ///     }
420    /// }
421    ///
422    /// impl ProvideCredentials for MyCustomProvider {
423    ///   fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials where Self: 'a {
424    ///         future::ProvideCredentials::new(self.load_credentials())
425    ///     }
426    /// }
427    ///
428    /// # if cfg!(feature = "rustls") {
429    /// let provider = ProfileFileCredentialsProvider::builder()
430    ///     .with_custom_provider("Custom", MyCustomProvider)
431    ///     .build();
432    /// # }
433    /// ```
434    pub fn with_custom_provider(
435        mut self,
436        name: impl Into<Cow<'static, str>>,
437        provider: impl ProvideCredentials + 'static,
438    ) -> Self {
439        self.custom_providers
440            .insert(name.into(), Arc::new(provider));
441        self
442    }
443
444    /// Override the profile name used by the [`ProfileFileCredentialsProvider`]
445    pub fn profile_name(mut self, profile_name: impl Into<String>) -> Self {
446        self.profile_override = Some(profile_name.into());
447        self
448    }
449
450    /// Set the profile file that should be used by the [`ProfileFileCredentialsProvider`]
451    #[allow(deprecated)]
452    pub fn profile_files(mut self, profile_files: ProfileFiles) -> Self {
453        self.profile_files = Some(profile_files);
454        self
455    }
456
457    /// Builds a [`ProfileFileCredentialsProvider`]
458    pub fn build(self) -> ProfileFileCredentialsProvider {
459        let build_span = tracing::debug_span!("build_profile_file_credentials_provider");
460        let _enter = build_span.enter();
461        let conf = self
462            .provider_config
463            .unwrap_or_default()
464            .with_profile_config(self.profile_files, self.profile_override);
465        let mut named_providers = self.custom_providers.clone();
466        named_providers
467            .entry("Environment".into())
468            .or_insert_with(|| {
469                Arc::new(crate::environment::credentials::EnvironmentVariableCredentialsProvider::new_with_env(
470                    conf.env(),
471                ))
472            });
473
474        named_providers
475            .entry("Ec2InstanceMetadata".into())
476            .or_insert_with(|| {
477                Arc::new(
478                    crate::imds::credentials::ImdsCredentialsProvider::builder()
479                        .configure(&conf)
480                        .build(),
481                )
482            });
483
484        named_providers
485            .entry("EcsContainer".into())
486            .or_insert_with(|| {
487                Arc::new(
488                    crate::ecs::EcsCredentialsProvider::builder()
489                        .configure(&conf)
490                        .build(),
491                )
492            });
493        let factory = exec::named::NamedProviderFactory::new(named_providers);
494
495        ProfileFileCredentialsProvider {
496            config: Arc::new(Config {
497                factory,
498                provider_config: conf,
499            }),
500            inner_provider: ErrorTakingOnceCell::new(),
501        }
502    }
503}
504
505async fn build_provider_chain(
506    config: Arc<Config>,
507) -> Result<exec::ProviderChain, ProfileFileError> {
508    let profile_set = config
509        .provider_config
510        .try_profile()
511        .await
512        .map_err(|parse_err| ProfileFileError::InvalidProfile(parse_err.clone()))?;
513    let repr = repr::resolve_chain(profile_set)?;
514    tracing::info!(chain = ?repr, "constructed abstract provider from config file");
515    exec::ProviderChain::from_repr(&config.provider_config, repr, &config.factory)
516}
517
518#[derive(Debug)]
519struct ChainProvider {
520    config: Arc<Config>,
521    chain: Option<Arc<exec::ProviderChain>>,
522}
523
524impl ChainProvider {
525    async fn provide_credentials(&self) -> Result<Credentials, CredentialsError> {
526        // Can't borrow `self` across an await point, or else we lose `Send` on the returned future
527        let config = self.config.clone();
528        let chain = self.chain.clone();
529
530        if let Some(chain) = chain {
531            let mut creds = match chain
532                .base()
533                .provide_credentials()
534                .instrument(tracing::debug_span!("load_base_credentials"))
535                .await
536            {
537                Ok(creds) => {
538                    tracing::info!(creds = ?creds, "loaded base credentials");
539                    creds
540                }
541                Err(e) => {
542                    tracing::warn!(error = %DisplayErrorContext(&e), "failed to load base credentials");
543                    return Err(CredentialsError::provider_error(e));
544                }
545            };
546
547            // we want to create `SdkConfig` _after_ we have resolved the profile or else
548            // we won't get things like `service_config()` set appropriately.
549            let sdk_config = config.provider_config.client_config();
550            for provider in chain.chain().iter() {
551                let next_creds = provider
552                    .credentials(creds, &sdk_config)
553                    .instrument(tracing::debug_span!("load_assume_role", provider = ?provider))
554                    .await;
555                match next_creds {
556                    Ok(next_creds) => {
557                        tracing::info!(creds = ?next_creds, "loaded assume role credentials");
558                        creds = next_creds
559                    }
560                    Err(e) => {
561                        tracing::warn!(provider = ?provider, "failed to load assume role credentials");
562                        return Err(CredentialsError::provider_error(e));
563                    }
564                }
565            }
566            Ok(creds)
567        } else {
568            Err(CredentialsError::not_loaded_no_source())
569        }
570    }
571}
572
573#[cfg(test)]
574mod test {
575    use crate::profile::credentials::Builder;
576    use aws_credential_types::provider::ProvideCredentials;
577
578    macro_rules! make_test {
579        ($name: ident) => {
580            #[tokio::test]
581            async fn $name() {
582                let _ = crate::test_case::TestEnvironment::from_dir(
583                    concat!("./test-data/profile-provider/", stringify!($name)),
584                    crate::test_case::test_credentials_provider(|config| async move {
585                        Builder::default()
586                            .configure(&config)
587                            .build()
588                            .provide_credentials()
589                            .await
590                    }),
591                )
592                .await
593                .unwrap()
594                .execute()
595                .await;
596            }
597        };
598    }
599
600    make_test!(e2e_assume_role);
601    make_test!(e2e_fips_and_dual_stack_sts);
602    make_test!(empty_config);
603    make_test!(retry_on_error);
604    make_test!(invalid_config);
605    make_test!(region_override);
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);
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_failure);
612    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is disabled on Windows because it uses Unix-style paths
613    #[cfg(all(feature = "credentials-process", not(windows)))]
614    make_test!(credential_process_account_id_fallback);
615    #[cfg(feature = "credentials-process")]
616    make_test!(credential_process_invalid);
617    #[cfg(feature = "sso")]
618    make_test!(sso_credentials);
619    #[cfg(feature = "sso")]
620    make_test!(invalid_sso_credentials_config);
621    #[cfg(feature = "sso")]
622    make_test!(sso_override_global_env_url);
623    #[cfg(feature = "sso")]
624    make_test!(sso_token);
625
626    make_test!(assume_role_override_global_env_url);
627    make_test!(assume_role_override_service_env_url);
628    make_test!(assume_role_override_global_profile_url);
629    make_test!(assume_role_override_service_profile_url);
630}
631
632#[cfg(all(test, feature = "sso"))]
633mod sso_tests {
634    use crate::{profile::credentials::Builder, provider_config::ProviderConfig};
635    use aws_credential_types::credential_feature::AwsCredentialFeature;
636    use aws_credential_types::provider::ProvideCredentials;
637    use aws_sdk_sso::config::RuntimeComponents;
638    use aws_smithy_runtime_api::client::{
639        http::{
640            HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings,
641            SharedHttpConnector,
642        },
643        orchestrator::{HttpRequest, HttpResponse},
644    };
645    use aws_smithy_types::body::SdkBody;
646    use aws_types::os_shim_internal::{Env, Fs};
647    use std::collections::HashMap;
648
649    #[derive(Debug)]
650    struct ClientInner {
651        expected_token: &'static str,
652    }
653    impl HttpConnector for ClientInner {
654        fn call(&self, request: HttpRequest) -> HttpConnectorFuture {
655            assert_eq!(
656                self.expected_token,
657                request.headers().get("x-amz-sso_bearer_token").unwrap()
658            );
659            HttpConnectorFuture::ready(Ok(HttpResponse::new(
660                    200.try_into().unwrap(),
661                    SdkBody::from("{\"roleCredentials\":{\"accessKeyId\":\"ASIARTESTID\",\"secretAccessKey\":\"TESTSECRETKEY\",\"sessionToken\":\"TESTSESSIONTOKEN\",\"expiration\": 1651516560000}}"),
662                )))
663        }
664    }
665    #[derive(Debug)]
666    struct Client {
667        inner: SharedHttpConnector,
668    }
669    impl Client {
670        fn new(expected_token: &'static str) -> Self {
671            Self {
672                inner: SharedHttpConnector::new(ClientInner { expected_token }),
673            }
674        }
675    }
676    impl HttpClient for Client {
677        fn http_connector(
678            &self,
679            _settings: &HttpConnectorSettings,
680            _components: &RuntimeComponents,
681        ) -> SharedHttpConnector {
682            self.inner.clone()
683        }
684    }
685
686    fn create_test_fs() -> Fs {
687        Fs::from_map({
688            let mut map = HashMap::new();
689            map.insert(
690                "/home/.aws/config".to_string(),
691                br#"
692[profile default]
693sso_session = dev
694sso_account_id = 012345678901
695sso_role_name = SampleRole
696region = us-east-1
697
698[sso-session dev]
699sso_region = us-east-1
700sso_start_url = https://d-abc123.awsapps.com/start
701                "#
702                .to_vec(),
703            );
704            map.insert(
705                "/home/.aws/sso/cache/34c6fceca75e456f25e7e99531e2425c6c1de443.json".to_string(),
706                br#"
707                {
708                    "accessToken": "secret-access-token",
709                    "expiresAt": "2199-11-14T04:05:45Z",
710                    "refreshToken": "secret-refresh-token",
711                    "clientId": "ABCDEFG323242423121312312312312312",
712                    "clientSecret": "ABCDE123",
713                    "registrationExpiresAt": "2199-03-06T19:53:17Z",
714                    "region": "us-east-1",
715                    "startUrl": "https://d-abc123.awsapps.com/start"
716                }
717                "#
718                .to_vec(),
719            );
720            map
721        })
722    }
723
724    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is ignored on Windows because it uses Unix-style paths
725    #[cfg_attr(windows, ignore)]
726    // In order to preserve the SSO token cache, the inner provider must only
727    // be created once, rather than once per credential resolution.
728    #[tokio::test]
729    async fn create_inner_provider_exactly_once() {
730        let fs = create_test_fs();
731
732        let provider_config = ProviderConfig::empty()
733            .with_fs(fs.clone())
734            .with_env(Env::from_slice(&[("HOME", "/home")]))
735            .with_http_client(Client::new("secret-access-token"));
736        let provider = Builder::default().configure(&provider_config).build();
737
738        let first_creds = provider.provide_credentials().await.unwrap();
739
740        // Write to the token cache with an access token that won't match the fake client's
741        // expected access token, and thus, won't return SSO credentials.
742        fs.write(
743            "/home/.aws/sso/cache/34c6fceca75e456f25e7e99531e2425c6c1de443.json",
744            r#"
745            {
746                "accessToken": "NEW!!secret-access-token",
747                "expiresAt": "2199-11-14T04:05:45Z",
748                "refreshToken": "secret-refresh-token",
749                "clientId": "ABCDEFG323242423121312312312312312",
750                "clientSecret": "ABCDE123",
751                "registrationExpiresAt": "2199-03-06T19:53:17Z",
752                "region": "us-east-1",
753                "startUrl": "https://d-abc123.awsapps.com/start"
754            }
755            "#,
756        )
757        .await
758        .unwrap();
759
760        // Loading credentials will still work since the SSOTokenProvider should have only
761        // been created once, and thus, the correct token is still in an in-memory cache.
762        let second_creds = provider
763            .provide_credentials()
764            .await
765            .expect("used cached token instead of loading from the file system");
766        assert_eq!(first_creds, second_creds);
767
768        // Now create a new provider, which should use the new cached token value from the file system
769        // since it won't have the in-memory cache. We do this just to verify that the FS mutation above
770        // actually worked correctly.
771        let provider_config = ProviderConfig::empty()
772            .with_fs(fs.clone())
773            .with_env(Env::from_slice(&[("HOME", "/home")]))
774            .with_http_client(Client::new("NEW!!secret-access-token"));
775        let provider = Builder::default().configure(&provider_config).build();
776        let third_creds = provider.provide_credentials().await.unwrap();
777        assert_eq!(second_creds, third_creds);
778    }
779
780    #[cfg_attr(windows, ignore)]
781    #[tokio::test]
782    async fn credential_feature() {
783        let fs = create_test_fs();
784
785        let provider_config = ProviderConfig::empty()
786            .with_fs(fs.clone())
787            .with_env(Env::from_slice(&[("HOME", "/home")]))
788            .with_http_client(Client::new("secret-access-token"));
789        let provider = Builder::default().configure(&provider_config).build();
790
791        let creds = provider.provide_credentials().await.unwrap();
792
793        assert_eq!(
794            &vec![
795                AwsCredentialFeature::CredentialsSso,
796                AwsCredentialFeature::CredentialsProfile
797            ],
798            creds.get_property::<Vec<AwsCredentialFeature>>().unwrap()
799        )
800    }
801}