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