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