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/// ### Credentials from a console session
136///
137/// An existing AWS Console session can be used to provide credentials.
138///
139/// ```ini
140/// [default]
141/// login_session = arn:aws:iam::0123456789012:user/Admin
142/// ```
143///
144#[doc = include_str!("location_of_profile_files.md")]
145#[derive(Debug)]
146pub struct ProfileFileCredentialsProvider {
147    config: Arc<Config>,
148    inner_provider: ErrorTakingOnceCell<ChainProvider, CredentialsError>,
149}
150
151#[derive(Debug)]
152struct Config {
153    factory: exec::named::NamedProviderFactory,
154    provider_config: ProviderConfig,
155}
156
157impl ProfileFileCredentialsProvider {
158    /// Builder for this credentials provider
159    pub fn builder() -> Builder {
160        Builder::default()
161    }
162
163    async fn load_credentials(&self) -> provider::Result {
164        // The inner provider needs to be cached across successive calls to load_credentials
165        // since the base providers can potentially have information cached in their instances.
166        // For example, the SsoCredentialsProvider maintains an in-memory expiring token cache.
167        let inner_provider = self
168            .inner_provider
169            .get_or_init(
170                {
171                    let config = self.config.clone();
172                    move || async move {
173                        match build_provider_chain(config.clone()).await {
174                            Ok(chain) => Ok(ChainProvider {
175                                config: config.clone(),
176                                chain: Some(Arc::new(chain)),
177                            }),
178                            Err(err) => match err {
179                                ProfileFileError::NoProfilesDefined
180                                | ProfileFileError::ProfileDidNotContainCredentials { .. } => {
181                                    Ok(ChainProvider {
182                                        config: config.clone(),
183                                        chain: None,
184                                    })
185                                }
186                                _ => Err(CredentialsError::invalid_configuration(format!(
187                                    "ProfileFile provider could not be built: {}",
188                                    &err
189                                ))),
190                            },
191                        }
192                    }
193                },
194                CredentialsError::unhandled(
195                    "profile file credentials provider initialization error already taken",
196                ),
197            )
198            .await?;
199        inner_provider.provide_credentials().await.map(|mut creds| {
200            creds
201                .get_property_mut_or_default::<Vec<AwsCredentialFeature>>()
202                .push(AwsCredentialFeature::CredentialsProfile);
203            creds
204        })
205    }
206}
207
208impl ProvideCredentials for ProfileFileCredentialsProvider {
209    fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a>
210    where
211        Self: 'a,
212    {
213        future::ProvideCredentials::new(self.load_credentials())
214    }
215}
216
217/// An Error building a Credential source from an AWS Profile
218#[derive(Debug)]
219#[non_exhaustive]
220pub enum ProfileFileError {
221    /// The profile was not a valid AWS profile
222    #[non_exhaustive]
223    InvalidProfile(ProfileFileLoadError),
224
225    /// No profiles existed (the profile was empty)
226    #[non_exhaustive]
227    NoProfilesDefined,
228
229    /// The profile did not contain any credential information
230    #[non_exhaustive]
231    ProfileDidNotContainCredentials {
232        /// The name of the profile
233        profile: String,
234    },
235
236    /// The profile contained an infinite loop of `source_profile` references
237    #[non_exhaustive]
238    CredentialLoop {
239        /// Vec of profiles leading to the loop
240        profiles: Vec<String>,
241        /// The next profile that caused the loop
242        next: String,
243    },
244
245    /// The profile was missing a credential source
246    #[non_exhaustive]
247    MissingCredentialSource {
248        /// The name of the profile
249        profile: String,
250        /// Error message
251        message: Cow<'static, str>,
252    },
253    /// The profile contained an invalid credential source
254    #[non_exhaustive]
255    InvalidCredentialSource {
256        /// The name of the profile
257        profile: String,
258        /// Error message
259        message: Cow<'static, str>,
260    },
261    /// The profile referred to a another profile by name that was not defined
262    #[non_exhaustive]
263    MissingProfile {
264        /// The name of the profile
265        profile: String,
266        /// Error message
267        message: Cow<'static, str>,
268    },
269    /// The profile referred to `credential_source` that was not defined
270    #[non_exhaustive]
271    UnknownProvider {
272        /// The name of the provider
273        name: String,
274    },
275
276    /// Feature not enabled
277    #[non_exhaustive]
278    FeatureNotEnabled {
279        /// The feature or comma delimited list of features that must be enabled
280        feature: Cow<'static, str>,
281        /// Additional information about the missing feature
282        message: Option<Cow<'static, str>>,
283    },
284
285    /// Missing sso-session section in config
286    #[non_exhaustive]
287    MissingSsoSession {
288        /// The name of the profile that specified `sso_session`
289        profile: String,
290        /// SSO session name
291        sso_session: String,
292    },
293
294    /// Invalid SSO configuration
295    #[non_exhaustive]
296    InvalidSsoConfig {
297        /// The name of the profile that the error originates in
298        profile: String,
299        /// Error message
300        message: Cow<'static, str>,
301    },
302
303    /// Profile is intended to be used in the token provider chain rather
304    /// than in the credentials chain.
305    #[non_exhaustive]
306    TokenProviderConfig {},
307}
308
309impl ProfileFileError {
310    fn missing_field(profile: &Profile, field: &'static str) -> Self {
311        ProfileFileError::MissingProfile {
312            profile: profile.name().to_string(),
313            message: format!("`{field}` was missing").into(),
314        }
315    }
316}
317
318impl Error for ProfileFileError {
319    fn source(&self) -> Option<&(dyn Error + 'static)> {
320        match self {
321            ProfileFileError::InvalidProfile(err) => Some(err),
322            _ => None,
323        }
324    }
325}
326
327impl Display for ProfileFileError {
328    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
329        match self {
330            ProfileFileError::InvalidProfile(err) => {
331                write!(f, "invalid profile: {err}")
332            }
333            ProfileFileError::CredentialLoop { profiles, next } => write!(
334                f,
335                "profile formed an infinite loop. first we loaded {profiles:?}, \
336            then attempted to reload {next}",
337            ),
338            ProfileFileError::MissingCredentialSource { profile, message } => {
339                write!(f, "missing credential source in `{profile}`: {message}")
340            }
341            ProfileFileError::InvalidCredentialSource { profile, message } => {
342                write!(f, "invalid credential source in `{profile}`: {message}")
343            }
344            ProfileFileError::MissingProfile { profile, message } => {
345                write!(f, "profile `{profile}` was not defined: {message}")
346            }
347            ProfileFileError::UnknownProvider { name } => write!(
348                f,
349                "profile referenced `{name}` provider but that provider is not supported",
350            ),
351            ProfileFileError::NoProfilesDefined => write!(f, "No profiles were defined"),
352            ProfileFileError::ProfileDidNotContainCredentials { profile } => write!(
353                f,
354                "profile `{profile}` did not contain credential information"
355            ),
356            ProfileFileError::FeatureNotEnabled { feature, message } => {
357                let message = message.as_deref().unwrap_or_default();
358                write!(
359                    f,
360                    "This behavior requires following cargo feature(s) enabled: {feature}. {message}",
361                )
362            }
363            ProfileFileError::MissingSsoSession {
364                profile,
365                sso_session,
366            } => {
367                write!(f, "sso-session named `{sso_session}` (referenced by profile `{profile}`) was not found")
368            }
369            ProfileFileError::InvalidSsoConfig { profile, message } => {
370                write!(f, "profile `{profile}` has invalid SSO config: {message}")
371            }
372            ProfileFileError::TokenProviderConfig { .. } => {
373                write!(
374                    f,
375                    "selected profile will resolve an access token instead of credentials \
376                     since it doesn't have `sso_account_id` and `sso_role_name` set. Specify both \
377                     `sso_account_id` and `sso_role_name` to let this profile resolve credentials."
378                )
379            }
380        }
381    }
382}
383
384/// Builder for [`ProfileFileCredentialsProvider`]
385#[derive(Debug, Default)]
386pub struct Builder {
387    provider_config: Option<ProviderConfig>,
388    profile_override: Option<String>,
389    #[allow(deprecated)]
390    profile_files: Option<ProfileFiles>,
391    custom_providers: HashMap<Cow<'static, str>, Arc<dyn ProvideCredentials>>,
392}
393
394impl Builder {
395    /// Override the configuration for the [`ProfileFileCredentialsProvider`]
396    ///
397    /// # Examples
398    ///
399    /// ```no_run
400    /// # async fn test() {
401    /// use aws_config::profile::ProfileFileCredentialsProvider;
402    /// use aws_config::provider_config::ProviderConfig;
403    /// let provider = ProfileFileCredentialsProvider::builder()
404    ///     .configure(&ProviderConfig::with_default_region().await)
405    ///     .build();
406    /// # }
407    /// ```
408    pub fn configure(mut self, provider_config: &ProviderConfig) -> Self {
409        self.provider_config = Some(provider_config.clone());
410        self
411    }
412
413    /// Adds a custom credential source
414    ///
415    /// # Examples
416    ///
417    /// ```no_run
418    /// use aws_credential_types::provider::{self, future, ProvideCredentials};
419    /// use aws_config::profile::ProfileFileCredentialsProvider;
420    /// #[derive(Debug)]
421    /// struct MyCustomProvider;
422    /// impl MyCustomProvider {
423    ///     async fn load_credentials(&self) -> provider::Result {
424    ///         todo!()
425    ///     }
426    /// }
427    ///
428    /// impl ProvideCredentials for MyCustomProvider {
429    ///   fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials where Self: 'a {
430    ///         future::ProvideCredentials::new(self.load_credentials())
431    ///     }
432    /// }
433    ///
434    /// # if cfg!(feature = "rustls") {
435    /// let provider = ProfileFileCredentialsProvider::builder()
436    ///     .with_custom_provider("Custom", MyCustomProvider)
437    ///     .build();
438    /// # }
439    /// ```
440    pub fn with_custom_provider(
441        mut self,
442        name: impl Into<Cow<'static, str>>,
443        provider: impl ProvideCredentials + 'static,
444    ) -> Self {
445        self.custom_providers
446            .insert(name.into(), Arc::new(provider));
447        self
448    }
449
450    /// Override the profile name used by the [`ProfileFileCredentialsProvider`]
451    pub fn profile_name(mut self, profile_name: impl Into<String>) -> Self {
452        self.profile_override = Some(profile_name.into());
453        self
454    }
455
456    /// Set the profile file that should be used by the [`ProfileFileCredentialsProvider`]
457    #[allow(deprecated)]
458    pub fn profile_files(mut self, profile_files: ProfileFiles) -> Self {
459        self.profile_files = Some(profile_files);
460        self
461    }
462
463    /// Builds a [`ProfileFileCredentialsProvider`]
464    pub fn build(self) -> ProfileFileCredentialsProvider {
465        let build_span = tracing::debug_span!("build_profile_file_credentials_provider");
466        let _enter = build_span.enter();
467        let conf = self
468            .provider_config
469            .unwrap_or_default()
470            .with_profile_config(self.profile_files, self.profile_override);
471        let mut named_providers = self.custom_providers.clone();
472        named_providers
473            .entry("Environment".into())
474            .or_insert_with(|| {
475                Arc::new(crate::environment::credentials::EnvironmentVariableCredentialsProvider::new_with_env(
476                    conf.env(),
477                ))
478            });
479
480        named_providers
481            .entry("Ec2InstanceMetadata".into())
482            .or_insert_with(|| {
483                Arc::new(
484                    crate::imds::credentials::ImdsCredentialsProvider::builder()
485                        .configure(&conf)
486                        .build(),
487                )
488            });
489
490        named_providers
491            .entry("EcsContainer".into())
492            .or_insert_with(|| {
493                Arc::new(
494                    crate::ecs::EcsCredentialsProvider::builder()
495                        .configure(&conf)
496                        .build(),
497                )
498            });
499        let factory = exec::named::NamedProviderFactory::new(named_providers);
500
501        ProfileFileCredentialsProvider {
502            config: Arc::new(Config {
503                factory,
504                provider_config: conf,
505            }),
506            inner_provider: ErrorTakingOnceCell::new(),
507        }
508    }
509}
510
511async fn build_provider_chain(
512    config: Arc<Config>,
513) -> Result<exec::ProviderChain, ProfileFileError> {
514    let profile_set = config
515        .provider_config
516        .try_profile()
517        .await
518        .map_err(|parse_err| ProfileFileError::InvalidProfile(parse_err.clone()))?;
519    let repr = repr::resolve_chain(profile_set)?;
520    tracing::info!(chain = ?repr, "constructed abstract provider from config file");
521    exec::ProviderChain::from_repr(&config.provider_config, repr, &config.factory)
522}
523
524#[derive(Debug)]
525struct ChainProvider {
526    config: Arc<Config>,
527    chain: Option<Arc<exec::ProviderChain>>,
528}
529
530impl ChainProvider {
531    async fn provide_credentials(&self) -> Result<Credentials, CredentialsError> {
532        // Can't borrow `self` across an await point, or else we lose `Send` on the returned future
533        let config = self.config.clone();
534        let chain = self.chain.clone();
535
536        if let Some(chain) = chain {
537            let mut creds = match chain
538                .base()
539                .provide_credentials()
540                .instrument(tracing::debug_span!("load_base_credentials"))
541                .await
542            {
543                Ok(creds) => {
544                    tracing::info!(creds = ?creds, "loaded base credentials");
545                    creds
546                }
547                Err(e) => {
548                    tracing::warn!(error = %DisplayErrorContext(&e), "failed to load base credentials");
549                    return Err(CredentialsError::provider_error(e));
550                }
551            };
552
553            // we want to create `SdkConfig` _after_ we have resolved the profile or else
554            // we won't get things like `service_config()` set appropriately.
555            //
556            // Resolve FIPS/dual-stack from the profile if they weren't set explicitly on the
557            // `ProviderConfig` (e.g. when `ProfileFileCredentialsProvider` is constructed
558            // directly via its builder, bypassing `ConfigLoader::load`).
559            let mut sdk_config_builder = config.provider_config.client_config().into_builder();
560            if config.provider_config.use_fips().is_none() {
561                sdk_config_builder.set_use_fips(
562                    crate::default_provider::use_fips::use_fips_provider(&config.provider_config)
563                        .await,
564                );
565            }
566            if config.provider_config.use_dual_stack().is_none() {
567                sdk_config_builder.set_use_dual_stack(
568                    crate::default_provider::use_dual_stack::use_dual_stack_provider(
569                        &config.provider_config,
570                    )
571                    .await,
572                );
573            }
574            let sdk_config = sdk_config_builder.build();
575            for provider in chain.chain().iter() {
576                let next_creds = provider
577                    .credentials(creds, &sdk_config)
578                    .instrument(tracing::debug_span!("load_assume_role", provider = ?provider))
579                    .await;
580                match next_creds {
581                    Ok(next_creds) => {
582                        tracing::info!(creds = ?next_creds, "loaded assume role credentials");
583                        creds = next_creds
584                    }
585                    Err(e) => {
586                        tracing::warn!(provider = ?provider, "failed to load assume role credentials");
587                        return Err(CredentialsError::provider_error(e));
588                    }
589                }
590            }
591            Ok(creds)
592        } else {
593            Err(CredentialsError::not_loaded_no_source())
594        }
595    }
596}
597
598#[cfg(test)]
599mod test {
600    use crate::profile::credentials::Builder;
601    use aws_credential_types::provider::ProvideCredentials;
602
603    macro_rules! make_test {
604        ($name: ident) => {
605            #[tokio::test]
606            async fn $name() {
607                let _ = crate::test_case::TestEnvironment::from_dir(
608                    concat!("./test-data/profile-provider/", stringify!($name)),
609                    crate::test_case::test_credentials_provider(|config| async move {
610                        Builder::default()
611                            .configure(&config)
612                            .build()
613                            .provide_credentials()
614                            .await
615                    }),
616                )
617                .await
618                .unwrap()
619                .execute()
620                .await;
621            }
622        };
623    }
624
625    make_test!(e2e_assume_role);
626    make_test!(e2e_fips_and_dual_stack_sts);
627    make_test!(empty_config);
628    make_test!(retry_on_error);
629    make_test!(invalid_config);
630    make_test!(region_override);
631    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is disabled on Windows because it uses Unix-style paths
632    #[cfg(all(feature = "credentials-process", not(windows)))]
633    make_test!(credential_process);
634    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is disabled on Windows because it uses Unix-style paths
635    #[cfg(all(feature = "credentials-process", not(windows)))]
636    make_test!(credential_process_failure);
637    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is disabled on Windows because it uses Unix-style paths
638    #[cfg(all(feature = "credentials-process", not(windows)))]
639    make_test!(credential_process_account_id_fallback);
640    #[cfg(feature = "credentials-process")]
641    make_test!(credential_process_invalid);
642    #[cfg(feature = "sso")]
643    make_test!(sso_credentials);
644    #[cfg(feature = "sso")]
645    make_test!(invalid_sso_credentials_config);
646    #[cfg(feature = "sso")]
647    make_test!(sso_override_global_env_url);
648    #[cfg(feature = "sso")]
649    make_test!(sso_token);
650
651    make_test!(assume_role_override_global_env_url);
652    make_test!(assume_role_override_service_env_url);
653    make_test!(assume_role_override_global_profile_url);
654    make_test!(assume_role_override_service_profile_url);
655
656    /// Regression test for https://github.com/smithy-lang/smithy-rs/issues/4614:
657    /// when building `ProfileFileCredentialsProvider` directly via its builder (bypassing
658    /// `ConfigLoader::load`), the profile's `use_fips_endpoint` and `use_dualstack_endpoint`
659    /// settings must still propagate to the internal STS client used for assume-role.
660    #[tokio::test]
661    async fn profile_use_fips_endpoint_propagates_to_sts_client() {
662        #[allow(deprecated)]
663        use crate::profile::profile_file::{ProfileFileKind, ProfileFiles};
664        use crate::provider_config::ProviderConfig;
665        use aws_smithy_http_client::test_util::capture_request;
666        use aws_smithy_types::body::SdkBody;
667        use aws_types::os_shim_internal::Env;
668        use aws_types::region::Region;
669
670        let (http_client, rx) = capture_request(Some(
671            http::Response::builder()
672                .status(200)
673                .body(SdkBody::from(
674                    r#"<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
675                        <AssumeRoleResult>
676                          <Credentials>
677                            <AccessKeyId>ASIAFAKEKEY</AccessKeyId>
678                            <SecretAccessKey>fakesecret</SecretAccessKey>
679                            <SessionToken>fakesession</SessionToken>
680                            <Expiration>2099-01-01T00:00:00Z</Expiration>
681                          </Credentials>
682                        </AssumeRoleResult>
683                       </AssumeRoleResponse>"#,
684                ))
685                .unwrap(),
686        ));
687
688        let provider_config = ProviderConfig::empty()
689            .with_env(Env::from_slice(&[]))
690            .with_region(Some(Region::new("us-east-1")))
691            .with_http_client(http_client);
692
693        #[allow(deprecated)]
694        let profile_files = ProfileFiles::builder()
695            .with_contents(
696                ProfileFileKind::Config,
697                "[profile fips-test]\nuse_fips_endpoint = true\nuse_dualstack_endpoint = true\nregion = us-east-1\nrole_arn = arn:aws:iam::123456789012:role/FakeTestRole\nsource_profile = source\n\n[profile source]\naws_access_key_id = AKIAIOSFODNN7EXAMPLE\naws_secret_access_key = secret\n",
698            )
699            .build();
700
701        let provider = Builder::default()
702            .configure(&provider_config)
703            .profile_files(profile_files)
704            .profile_name("fips-test")
705            .build();
706
707        let _ = provider.provide_credentials().await;
708        let request = rx.expect_request();
709        let uri = request.uri().to_string();
710        // FIPS + dual-stack STS endpoint has the form `sts-fips.<region>.api.aws`.
711        assert!(
712            uri.contains("sts-fips.us-east-1.api.aws"),
713            "expected STS FIPS + dual-stack endpoint, got: {uri}"
714        );
715    }
716}
717
718#[cfg(all(test, feature = "sso"))]
719mod sso_tests {
720    use crate::{profile::credentials::Builder, provider_config::ProviderConfig};
721    use aws_credential_types::credential_feature::AwsCredentialFeature;
722    use aws_credential_types::provider::ProvideCredentials;
723    use aws_sdk_sso::config::RuntimeComponents;
724    use aws_smithy_runtime_api::client::{
725        http::{
726            HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings,
727            SharedHttpConnector,
728        },
729        orchestrator::{HttpRequest, HttpResponse},
730    };
731    use aws_smithy_types::body::SdkBody;
732    use aws_types::os_shim_internal::{Env, Fs};
733    use std::collections::HashMap;
734
735    #[derive(Debug)]
736    struct ClientInner {
737        expected_token: &'static str,
738    }
739    impl HttpConnector for ClientInner {
740        fn call(&self, request: HttpRequest) -> HttpConnectorFuture {
741            assert_eq!(
742                self.expected_token,
743                request.headers().get("x-amz-sso_bearer_token").unwrap()
744            );
745            HttpConnectorFuture::ready(Ok(HttpResponse::new(
746                    200.try_into().unwrap(),
747                    SdkBody::from("{\"roleCredentials\":{\"accessKeyId\":\"ASIARTESTID\",\"secretAccessKey\":\"TESTSECRETKEY\",\"sessionToken\":\"TESTSESSIONTOKEN\",\"expiration\": 1651516560000}}"),
748                )))
749        }
750    }
751    #[derive(Debug)]
752    struct Client {
753        inner: SharedHttpConnector,
754    }
755    impl Client {
756        fn new(expected_token: &'static str) -> Self {
757            Self {
758                inner: SharedHttpConnector::new(ClientInner { expected_token }),
759            }
760        }
761    }
762    impl HttpClient for Client {
763        fn http_connector(
764            &self,
765            _settings: &HttpConnectorSettings,
766            _components: &RuntimeComponents,
767        ) -> SharedHttpConnector {
768            self.inner.clone()
769        }
770    }
771
772    fn create_test_fs() -> Fs {
773        Fs::from_map({
774            let mut map = HashMap::new();
775            map.insert(
776                "/home/.aws/config".to_string(),
777                br#"
778[profile default]
779sso_session = dev
780sso_account_id = 012345678901
781sso_role_name = SampleRole
782region = us-east-1
783
784[sso-session dev]
785sso_region = us-east-1
786sso_start_url = https://d-abc123.awsapps.com/start
787                "#
788                .to_vec(),
789            );
790            map.insert(
791                "/home/.aws/sso/cache/34c6fceca75e456f25e7e99531e2425c6c1de443.json".to_string(),
792                br#"
793                {
794                    "accessToken": "secret-access-token",
795                    "expiresAt": "2199-11-14T04:05:45Z",
796                    "refreshToken": "secret-refresh-token",
797                    "clientId": "ABCDEFG323242423121312312312312312",
798                    "clientSecret": "ABCDE123",
799                    "registrationExpiresAt": "2199-03-06T19:53:17Z",
800                    "region": "us-east-1",
801                    "startUrl": "https://d-abc123.awsapps.com/start"
802                }
803                "#
804                .to_vec(),
805            );
806            map
807        })
808    }
809
810    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is ignored on Windows because it uses Unix-style paths
811    #[cfg_attr(windows, ignore)]
812    // In order to preserve the SSO token cache, the inner provider must only
813    // be created once, rather than once per credential resolution.
814    #[tokio::test]
815    async fn create_inner_provider_exactly_once() {
816        let fs = create_test_fs();
817
818        let provider_config = ProviderConfig::empty()
819            .with_fs(fs.clone())
820            .with_env(Env::from_slice(&[("HOME", "/home")]))
821            .with_http_client(Client::new("secret-access-token"));
822        let provider = Builder::default().configure(&provider_config).build();
823
824        let first_creds = provider.provide_credentials().await.unwrap();
825
826        // Write to the token cache with an access token that won't match the fake client's
827        // expected access token, and thus, won't return SSO credentials.
828        fs.write(
829            "/home/.aws/sso/cache/34c6fceca75e456f25e7e99531e2425c6c1de443.json",
830            r#"
831            {
832                "accessToken": "NEW!!secret-access-token",
833                "expiresAt": "2199-11-14T04:05:45Z",
834                "refreshToken": "secret-refresh-token",
835                "clientId": "ABCDEFG323242423121312312312312312",
836                "clientSecret": "ABCDE123",
837                "registrationExpiresAt": "2199-03-06T19:53:17Z",
838                "region": "us-east-1",
839                "startUrl": "https://d-abc123.awsapps.com/start"
840            }
841            "#,
842        )
843        .await
844        .unwrap();
845
846        // Loading credentials will still work since the SSOTokenProvider should have only
847        // been created once, and thus, the correct token is still in an in-memory cache.
848        let second_creds = provider
849            .provide_credentials()
850            .await
851            .expect("used cached token instead of loading from the file system");
852        assert_eq!(first_creds, second_creds);
853
854        // Now create a new provider, which should use the new cached token value from the file system
855        // since it won't have the in-memory cache. We do this just to verify that the FS mutation above
856        // actually worked correctly.
857        let provider_config = ProviderConfig::empty()
858            .with_fs(fs.clone())
859            .with_env(Env::from_slice(&[("HOME", "/home")]))
860            .with_http_client(Client::new("NEW!!secret-access-token"));
861        let provider = Builder::default().configure(&provider_config).build();
862        let third_creds = provider.provide_credentials().await.unwrap();
863        assert_eq!(second_creds, third_creds);
864    }
865
866    #[cfg_attr(windows, ignore)]
867    #[tokio::test]
868    async fn credential_feature() {
869        let fs = create_test_fs();
870
871        let provider_config = ProviderConfig::empty()
872            .with_fs(fs.clone())
873            .with_env(Env::from_slice(&[("HOME", "/home")]))
874            .with_http_client(Client::new("secret-access-token"));
875        let provider = Builder::default().configure(&provider_config).build();
876
877        let creds = provider.provide_credentials().await.unwrap();
878
879        assert_eq!(
880            &vec![
881                AwsCredentialFeature::CredentialsSso,
882                AwsCredentialFeature::CredentialsProfile
883            ],
884            creds.get_property::<Vec<AwsCredentialFeature>>().unwrap()
885        )
886    }
887}
888
889#[cfg(all(test, feature = "credentials-login"))]
890mod login_tests {
891    use crate::provider_config::ProviderConfig;
892    use aws_credential_types::provider::error::CredentialsError;
893    use aws_credential_types::provider::ProvideCredentials;
894    use aws_sdk_signin::config::RuntimeComponents;
895    use aws_smithy_runtime_api::client::{
896        http::{
897            HttpClient, HttpConnector, HttpConnectorFuture, HttpConnectorSettings,
898            SharedHttpConnector,
899        },
900        orchestrator::{HttpRequest, HttpResponse},
901    };
902    use aws_smithy_types::body::SdkBody;
903    use aws_types::os_shim_internal::{Env, Fs};
904    use std::collections::HashMap;
905    use std::sync::atomic::{AtomicUsize, Ordering};
906    use std::sync::Arc;
907
908    #[derive(Debug, Clone)]
909    struct TestClientInner {
910        call_count: Arc<AtomicUsize>,
911        response: Option<&'static str>,
912    }
913
914    impl HttpConnector for TestClientInner {
915        fn call(&self, _request: HttpRequest) -> HttpConnectorFuture {
916            self.call_count.fetch_add(1, Ordering::SeqCst);
917            if let Some(response) = self.response {
918                HttpConnectorFuture::ready(Ok(HttpResponse::new(
919                    200.try_into().unwrap(),
920                    SdkBody::from(response),
921                )))
922            } else {
923                HttpConnectorFuture::ready(Ok(HttpResponse::new(
924                    500.try_into().unwrap(),
925                    SdkBody::from("{\"error\":\"server_error\"}"),
926                )))
927            }
928        }
929    }
930
931    #[derive(Debug, Clone)]
932    struct TestClient {
933        inner: SharedHttpConnector,
934        call_count: Arc<AtomicUsize>,
935    }
936
937    impl TestClient {
938        fn new_success() -> Self {
939            let call_count = Arc::new(AtomicUsize::new(0));
940            let response = r#"{
941                "accessToken": {
942                    "accessKeyId": "ASIARTESTID",
943                    "secretAccessKey": "TESTSECRETKEY",
944                    "sessionToken": "TESTSESSIONTOKEN"
945                },
946                "expiresIn": 3600,
947                "refreshToken": "new-refresh-token"
948            }"#;
949            let inner = TestClientInner {
950                call_count: call_count.clone(),
951                response: Some(response),
952            };
953            Self {
954                inner: SharedHttpConnector::new(inner),
955                call_count,
956            }
957        }
958
959        fn new_error() -> Self {
960            let call_count = Arc::new(AtomicUsize::new(0));
961            let inner = TestClientInner {
962                call_count: call_count.clone(),
963                response: None,
964            };
965            Self {
966                inner: SharedHttpConnector::new(inner),
967                call_count,
968            }
969        }
970
971        fn call_count(&self) -> usize {
972            self.call_count.load(Ordering::SeqCst)
973        }
974    }
975
976    impl HttpClient for TestClient {
977        fn http_connector(
978            &self,
979            _settings: &HttpConnectorSettings,
980            _components: &RuntimeComponents,
981        ) -> SharedHttpConnector {
982            self.inner.clone()
983        }
984    }
985
986    fn create_test_fs_unexpired() -> Fs {
987        Fs::from_map({
988            let mut map = HashMap::new();
989            map.insert(
990                "/home/.aws/config".to_string(),
991                br#"
992[profile default]
993login_session = arn:aws:iam::0123456789012:user/Admin
994region = us-east-1
995                "#
996                .to_vec(),
997            );
998            map.insert(
999                "/home/.aws/login/cache/36db1d138ff460920374e4c3d8e01f53f9f73537e89c88d639f68393df0e2726.json".to_string(),
1000                br#"{
1001                    "accessToken": {
1002                        "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
1003                        "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
1004                        "sessionToken": "session-token",
1005                        "accountId": "012345678901",
1006                        "expiresAt": "2199-12-25T21:30:00Z"
1007                    },
1008                    "tokenType": "aws_sigv4",
1009                    "refreshToken": "refresh-token-value",
1010                    "identityToken": "identity-token-value",
1011                    "clientId": "aws:signin:::cli/same-device",
1012                    "dpopKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIFDZHUzOG1Pzq+6F0mjMlOSp1syN9LRPBuHMoCFXTcXhoAoGCCqGSM49\nAwEHoUQDQgAE9qhj+KtcdHj1kVgwxWWWw++tqoh7H7UHs7oXh8jBbgF47rrYGC+t\ndjiIaHK3dBvvdE7MGj5HsepzLm3Kj91bqA==\n-----END EC PRIVATE KEY-----\n"
1013                }"#
1014                .to_vec(),
1015            );
1016            map
1017        })
1018    }
1019
1020    fn create_test_fs_expired() -> Fs {
1021        Fs::from_map({
1022            let mut map = HashMap::new();
1023            map.insert(
1024                "/home/.aws/config".to_string(),
1025                br#"
1026[profile default]
1027login_session = arn:aws:iam::0123456789012:user/Admin
1028region = us-east-1
1029                "#
1030                .to_vec(),
1031            );
1032            map.insert(
1033                "/home/.aws/login/cache/36db1d138ff460920374e4c3d8e01f53f9f73537e89c88d639f68393df0e2726.json".to_string(),
1034                br#"{
1035                    "accessToken": {
1036                        "accessKeyId": "AKIAIOSFODNN7EXAMPLE",
1037                        "secretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
1038                        "sessionToken": "session-token",
1039                        "accountId": "012345678901",
1040                        "expiresAt": "2020-01-01T00:00:00Z"
1041                    },
1042                    "tokenType": "aws_sigv4",
1043                    "refreshToken": "refresh-token-value",
1044                    "identityToken": "identity-token-value",
1045                    "clientId": "aws:signin:::cli/same-device",
1046                    "dpopKey": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIFDZHUzOG1Pzq+6F0mjMlOSp1syN9LRPBuHMoCFXTcXhoAoGCCqGSM49\nAwEHoUQDQgAE9qhj+KtcdHj1kVgwxWWWw++tqoh7H7UHs7oXh8jBbgF47rrYGC+t\ndjiIaHK3dBvvdE7MGj5HsepzLm3Kj91bqA==\n-----END EC PRIVATE KEY-----\n"
1047                }"#
1048                .to_vec(),
1049            );
1050            map
1051        })
1052    }
1053
1054    #[cfg_attr(windows, ignore)]
1055    #[tokio::test]
1056    async fn unexpired_credentials_no_refresh() {
1057        let client = TestClient::new_success();
1058
1059        let provider_config = ProviderConfig::empty()
1060            .with_fs(create_test_fs_unexpired())
1061            .with_env(Env::from_slice(&[("HOME", "/home")]))
1062            .with_http_client(client.clone())
1063            .with_region(Some(aws_types::region::Region::new("us-east-1")));
1064
1065        let provider = crate::profile::credentials::Builder::default()
1066            .configure(&provider_config)
1067            .build();
1068
1069        let creds = provider.provide_credentials().await.unwrap();
1070        assert_eq!("AKIAIOSFODNN7EXAMPLE", creds.access_key_id());
1071        assert_eq!(0, client.call_count());
1072    }
1073
1074    #[cfg_attr(windows, ignore)]
1075    #[tokio::test]
1076    async fn expired_credentials_trigger_refresh() {
1077        let client = TestClient::new_success();
1078
1079        let provider_config = ProviderConfig::empty()
1080            .with_fs(create_test_fs_expired())
1081            .with_env(Env::from_slice(&[("HOME", "/home")]))
1082            .with_http_client(client.clone())
1083            .with_region(Some(aws_types::region::Region::new("us-east-1")));
1084
1085        let provider = crate::profile::credentials::Builder::default()
1086            .configure(&provider_config)
1087            .build();
1088
1089        let creds = provider.provide_credentials().await.unwrap();
1090        assert_eq!("ASIARTESTID", creds.access_key_id());
1091        assert_eq!(1, client.call_count());
1092    }
1093
1094    #[cfg_attr(windows, ignore)]
1095    #[tokio::test]
1096    async fn refresh_error_propagates() {
1097        let client = TestClient::new_error();
1098
1099        let provider_config = ProviderConfig::empty()
1100            .with_fs(create_test_fs_expired())
1101            .with_env(Env::from_slice(&[("HOME", "/home")]))
1102            .with_http_client(client)
1103            .with_region(Some(aws_types::region::Region::new("us-east-1")));
1104
1105        let provider = crate::profile::credentials::Builder::default()
1106            .configure(&provider_config)
1107            .build();
1108
1109        let err = provider
1110            .provide_credentials()
1111            .await
1112            .expect_err("should fail on refresh error");
1113
1114        match &err {
1115            CredentialsError::ProviderError(_) => {}
1116            _ => panic!("wrong error type"),
1117        }
1118    }
1119}