aws_config/imds/
credentials.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! IMDSv2 Credentials Provider
7//!
8//! # Important
9//! This credential provider will NOT fallback to IMDSv1. Ensure that IMDSv2 is enabled on your instances.
10
11use super::client::error::ImdsError;
12use crate::imds::{self, Client};
13use crate::json_credentials::{parse_json_credentials, JsonCredentials, RefreshableCredentials};
14use crate::provider_config::ProviderConfig;
15use aws_credential_types::credential_feature::AwsCredentialFeature;
16use aws_credential_types::provider::{self, error::CredentialsError, future, ProvideCredentials};
17use aws_credential_types::Credentials;
18use aws_smithy_async::time::SharedTimeSource;
19use aws_types::os_shim_internal::Env;
20use std::borrow::Cow;
21use std::error::Error as StdError;
22use std::fmt;
23use std::sync::{Arc, RwLock};
24use std::time::{Duration, SystemTime};
25
26const CREDENTIAL_EXPIRATION_INTERVAL: Duration = Duration::from_secs(10 * 60);
27const WARNING_FOR_EXTENDING_CREDENTIALS_EXPIRY: &str =
28    "Attempting credential expiration extension due to a credential service availability issue. \
29    A refresh of these credentials will be attempted again within the next";
30
31#[derive(Debug)]
32struct ImdsCommunicationError {
33    source: Box<dyn StdError + Send + Sync + 'static>,
34}
35
36impl fmt::Display for ImdsCommunicationError {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        write!(f, "could not communicate with IMDS")
39    }
40}
41
42impl StdError for ImdsCommunicationError {
43    fn source(&self) -> Option<&(dyn StdError + 'static)> {
44        Some(self.source.as_ref())
45    }
46}
47
48/// IMDSv2 Credentials Provider
49///
50/// _Note: This credentials provider will NOT fallback to the IMDSv1 flow._
51#[derive(Debug)]
52pub struct ImdsCredentialsProvider {
53    client: Client,
54    env: Env,
55    profile: Option<String>,
56    time_source: SharedTimeSource,
57    last_retrieved_credentials: Arc<RwLock<Option<Credentials>>>,
58}
59
60/// Builder for [`ImdsCredentialsProvider`]
61#[derive(Default, Debug)]
62pub struct Builder {
63    provider_config: Option<ProviderConfig>,
64    profile_override: Option<String>,
65    imds_override: Option<imds::Client>,
66    last_retrieved_credentials: Option<Credentials>,
67}
68
69impl Builder {
70    /// Override the configuration used for this provider
71    pub fn configure(mut self, provider_config: &ProviderConfig) -> Self {
72        self.provider_config = Some(provider_config.clone());
73        self
74    }
75
76    /// Override the [instance profile](instance-profile) used for this provider.
77    ///
78    /// When retrieving IMDS credentials, a call must first be made to
79    /// `<IMDS_BASE_URL>/latest/meta-data/iam/security-credentials/`. This returns the instance
80    /// profile used. By setting this parameter, retrieving the profile is skipped
81    /// and the provided value is used instead.
82    ///
83    /// [instance-profile]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#ec2-instance-profile
84    pub fn profile(mut self, profile: impl Into<String>) -> Self {
85        self.profile_override = Some(profile.into());
86        self
87    }
88
89    /// Override the IMDS client used for this provider
90    ///
91    /// The IMDS client will be loaded and configured via `~/.aws/config` and environment variables,
92    /// however, if necessary the entire client may be provided directly.
93    ///
94    /// For more information about IMDS client configuration loading see [`imds::Client`]
95    pub fn imds_client(mut self, client: imds::Client) -> Self {
96        self.imds_override = Some(client);
97        self
98    }
99
100    #[allow(dead_code)]
101    #[cfg(test)]
102    fn last_retrieved_credentials(mut self, credentials: Credentials) -> Self {
103        self.last_retrieved_credentials = Some(credentials);
104        self
105    }
106
107    /// Create an [`ImdsCredentialsProvider`] from this builder.
108    pub fn build(self) -> ImdsCredentialsProvider {
109        let provider_config = self.provider_config.unwrap_or_default();
110        let env = provider_config.env();
111        let client = self
112            .imds_override
113            .unwrap_or_else(|| imds::Client::builder().configure(&provider_config).build());
114        ImdsCredentialsProvider {
115            client,
116            env,
117            profile: self.profile_override,
118            time_source: provider_config.time_source(),
119            last_retrieved_credentials: Arc::new(RwLock::new(self.last_retrieved_credentials)),
120        }
121    }
122}
123
124mod codes {
125    pub(super) const ASSUME_ROLE_UNAUTHORIZED_ACCESS: &str = "AssumeRoleUnauthorizedAccess";
126}
127
128impl ProvideCredentials for ImdsCredentialsProvider {
129    fn provide_credentials<'a>(&'a self) -> future::ProvideCredentials<'a>
130    where
131        Self: 'a,
132    {
133        future::ProvideCredentials::new(self.credentials())
134    }
135
136    fn fallback_on_interrupt(&self) -> Option<Credentials> {
137        self.last_retrieved_credentials.read().unwrap().clone()
138    }
139}
140
141impl ImdsCredentialsProvider {
142    /// Builder for [`ImdsCredentialsProvider`]
143    pub fn builder() -> Builder {
144        Builder::default()
145    }
146
147    fn imds_disabled(&self) -> bool {
148        match self.env.get(super::env::EC2_METADATA_DISABLED) {
149            Ok(value) => value.eq_ignore_ascii_case("true"),
150            _ => false,
151        }
152    }
153
154    /// Retrieve the instance profile from IMDS
155    async fn get_profile_uncached(&self) -> Result<String, CredentialsError> {
156        match self
157            .client
158            .get("/latest/meta-data/iam/security-credentials/")
159            .await
160        {
161            Ok(profile) => Ok(profile.as_ref().into()),
162            Err(ImdsError::ErrorResponse(context))
163                if context.response().status().as_u16() == 404 =>
164            {
165                tracing::warn!(
166                    "received 404 from IMDS when loading profile information. \
167                    Hint: This instance may not have an IAM role associated."
168                );
169                Err(CredentialsError::not_loaded("received 404 from IMDS"))
170            }
171            Err(ImdsError::FailedToLoadToken(context)) if context.is_dispatch_failure() => {
172                Err(CredentialsError::not_loaded(ImdsCommunicationError {
173                    source: context.into_source().into(),
174                }))
175            }
176            Err(other) => Err(CredentialsError::provider_error(other)),
177        }
178    }
179
180    // Extend the cached expiration time if necessary
181    //
182    // This allows continued use of the credentials even when IMDS returns expired ones.
183    fn maybe_extend_expiration(&self, expiration: SystemTime) -> SystemTime {
184        let now = self.time_source.now();
185        // If credentials from IMDS are not stale, use them as they are.
186        if now < expiration {
187            return expiration;
188        }
189
190        let mut rng = fastrand::Rng::with_seed(
191            now.duration_since(SystemTime::UNIX_EPOCH)
192                .expect("now should be after UNIX EPOCH")
193                .as_secs(),
194        );
195        // Calculate credentials' refresh offset with jitter, which should be less than 15 minutes
196        // the smallest amount of time credentials are valid for.
197        // Setting it to something longer than that may have the risk of the credentials expiring
198        // before the next refresh.
199        let refresh_offset = CREDENTIAL_EXPIRATION_INTERVAL + Duration::from_secs(rng.u64(0..=300));
200        let new_expiry = now + refresh_offset;
201
202        tracing::warn!(
203            "{WARNING_FOR_EXTENDING_CREDENTIALS_EXPIRY} {:.2} minutes.",
204            refresh_offset.as_secs_f64() / 60.0,
205        );
206
207        new_expiry
208    }
209
210    async fn retrieve_credentials(&self) -> provider::Result {
211        if self.imds_disabled() {
212            let err = format!(
213                "IMDS disabled by {} env var set to `true`",
214                super::env::EC2_METADATA_DISABLED
215            );
216            tracing::debug!(err);
217            return Err(CredentialsError::not_loaded(err));
218        }
219        tracing::debug!("loading credentials from IMDS");
220        let profile: Cow<'_, str> = match &self.profile {
221            Some(profile) => profile.into(),
222            None => self.get_profile_uncached().await?.into(),
223        };
224        tracing::debug!(profile = %profile, "loaded profile");
225        let credentials = self
226            .client
227            .get(format!(
228                "/latest/meta-data/iam/security-credentials/{}",
229                profile
230            ))
231            .await
232            .map_err(CredentialsError::provider_error)?;
233        match parse_json_credentials(credentials.as_ref()) {
234            Ok(JsonCredentials::RefreshableCredentials(RefreshableCredentials {
235                access_key_id,
236                secret_access_key,
237                session_token,
238                account_id,
239                expiration,
240                ..
241            })) => {
242                // TODO(IMDSv2.X): Use `account_id` once the design is finalized
243                let _ = account_id;
244                let expiration = self.maybe_extend_expiration(expiration);
245                let creds = Credentials::new(
246                    access_key_id,
247                    secret_access_key,
248                    Some(session_token.to_string()),
249                    expiration.into(),
250                    "IMDSv2",
251                );
252                *self.last_retrieved_credentials.write().unwrap() = Some(creds.clone());
253                Ok(creds)
254            }
255            Ok(JsonCredentials::Error { code, message })
256                if code == codes::ASSUME_ROLE_UNAUTHORIZED_ACCESS =>
257            {
258                Err(CredentialsError::invalid_configuration(format!(
259                    "Incorrect IMDS/IAM configuration: [{}] {}. \
260                        Hint: Does this role have a trust relationship with EC2?",
261                    code, message
262                )))
263            }
264            Ok(JsonCredentials::Error { code, message }) => {
265                Err(CredentialsError::provider_error(format!(
266                    "Error retrieving credentials from IMDS: {} {}",
267                    code, message
268                )))
269            }
270            // got bad data from IMDS, should not occur during normal operation:
271            Err(invalid) => Err(CredentialsError::unhandled(invalid)),
272        }
273        .map(|mut creds| {
274            creds
275                .get_property_mut_or_default::<Vec<AwsCredentialFeature>>()
276                .push(AwsCredentialFeature::CredentialsImds);
277            creds
278        })
279    }
280
281    async fn credentials(&self) -> provider::Result {
282        match self.retrieve_credentials().await {
283            creds @ Ok(_) => creds,
284            // Any failure while retrieving credentials MUST NOT impede use of existing credentials.
285            err => match &*self.last_retrieved_credentials.read().unwrap() {
286                Some(creds) => Ok(creds.clone()),
287                _ => err,
288            },
289        }
290    }
291}
292
293#[cfg(test)]
294mod test {
295    use super::*;
296    use crate::imds::client::test::{
297        imds_request, imds_response, make_imds_client, token_request, token_response,
298    };
299    use crate::provider_config::ProviderConfig;
300    use aws_credential_types::credential_feature::AwsCredentialFeature;
301    use aws_credential_types::provider::ProvideCredentials;
302    use aws_smithy_async::test_util::instant_time_and_sleep;
303    use aws_smithy_http_client::test_util::{ReplayEvent, StaticReplayClient};
304    use aws_smithy_types::body::SdkBody;
305    use std::time::{Duration, UNIX_EPOCH};
306    use tracing_test::traced_test;
307
308    const TOKEN_A: &str = "token_a";
309
310    #[tokio::test]
311    async fn profile_is_not_cached() {
312        let http_client = StaticReplayClient::new(vec![
313            ReplayEvent::new(
314                token_request("http://169.254.169.254", 21600),
315                token_response(21600, TOKEN_A),
316            ),
317            ReplayEvent::new(
318                imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
319                imds_response(r#"profile-name"#),
320            ),
321            ReplayEvent::new(
322                imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
323                imds_response("{\n  \"Code\" : \"Success\",\n  \"LastUpdated\" : \"2021-09-20T21:42:26Z\",\n  \"Type\" : \"AWS-HMAC\",\n  \"AccessKeyId\" : \"ASIARTEST\",\n  \"SecretAccessKey\" : \"testsecret\",\n  \"Token\" : \"testtoken\",\n  \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}"),
324            ),
325            ReplayEvent::new(
326                imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
327                imds_response(r#"different-profile"#),
328            ),
329            ReplayEvent::new(
330                imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/different-profile", TOKEN_A),
331                imds_response("{\n  \"Code\" : \"Success\",\n  \"LastUpdated\" : \"2021-09-20T21:42:26Z\",\n  \"Type\" : \"AWS-HMAC\",\n  \"AccessKeyId\" : \"ASIARTEST2\",\n  \"SecretAccessKey\" : \"testsecret\",\n  \"Token\" : \"testtoken\",\n  \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}"),
332            ),
333        ]);
334        let client = ImdsCredentialsProvider::builder()
335            .imds_client(make_imds_client(&http_client))
336            .configure(&ProviderConfig::no_configuration())
337            .build();
338        let creds1 = client.provide_credentials().await.expect("valid creds");
339        let creds2 = client.provide_credentials().await.expect("valid creds");
340        assert_eq!(creds1.access_key_id(), "ASIARTEST");
341        assert_eq!(creds2.access_key_id(), "ASIARTEST2");
342        http_client.assert_requests_match(&[]);
343    }
344
345    #[tokio::test]
346    #[traced_test]
347    async fn credentials_not_stale_should_be_used_as_they_are() {
348        let http_client = StaticReplayClient::new(vec![
349            ReplayEvent::new(
350                token_request("http://169.254.169.254", 21600),
351                token_response(21600, TOKEN_A),
352            ),
353            ReplayEvent::new(
354                imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
355                imds_response(r#"profile-name"#),
356            ),
357            ReplayEvent::new(
358                imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
359                imds_response("{\n  \"Code\" : \"Success\",\n  \"LastUpdated\" : \"2021-09-20T21:42:26Z\",\n  \"Type\" : \"AWS-HMAC\",\n  \"AccessKeyId\" : \"ASIARTEST\",\n  \"SecretAccessKey\" : \"testsecret\",\n  \"Token\" : \"testtoken\",\n  \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}"),
360            ),
361        ]);
362
363        // set to 2021-09-21T04:16:50Z that makes returned credentials' expiry (2021-09-21T04:16:53Z)
364        // not stale
365        let time_of_request_to_fetch_credentials = UNIX_EPOCH + Duration::from_secs(1632197810);
366        let (time_source, sleep) = instant_time_and_sleep(time_of_request_to_fetch_credentials);
367
368        let provider_config = ProviderConfig::no_configuration()
369            .with_http_client(http_client.clone())
370            .with_sleep_impl(sleep)
371            .with_time_source(time_source);
372        let client = crate::imds::Client::builder()
373            .configure(&provider_config)
374            .build();
375        let provider = ImdsCredentialsProvider::builder()
376            .configure(&provider_config)
377            .imds_client(client)
378            .build();
379        let creds = provider.provide_credentials().await.expect("valid creds");
380        // The expiry should be equal to what is originally set (==2021-09-21T04:16:53Z).
381        assert_eq!(
382            creds.expiry(),
383            UNIX_EPOCH.checked_add(Duration::from_secs(1632197813))
384        );
385        http_client.assert_requests_match(&[]);
386
387        // There should not be logs indicating credentials are extended for stability.
388        assert!(!logs_contain(WARNING_FOR_EXTENDING_CREDENTIALS_EXPIRY));
389    }
390    #[tokio::test]
391    #[traced_test]
392    async fn expired_credentials_should_be_extended() {
393        let http_client = StaticReplayClient::new(vec![
394                ReplayEvent::new(
395                    token_request("http://169.254.169.254", 21600),
396                    token_response(21600, TOKEN_A),
397                ),
398                ReplayEvent::new(
399                    imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
400                    imds_response(r#"profile-name"#),
401                ),
402                ReplayEvent::new(
403                    imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
404                    imds_response("{\n  \"Code\" : \"Success\",\n  \"LastUpdated\" : \"2021-09-20T21:42:26Z\",\n  \"Type\" : \"AWS-HMAC\",\n  \"AccessKeyId\" : \"ASIARTEST\",\n  \"SecretAccessKey\" : \"testsecret\",\n  \"Token\" : \"testtoken\",\n  \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}"),
405                ),
406            ]);
407
408        // set to 2021-09-21T17:41:25Z that renders fetched credentials already expired (2021-09-21T04:16:53Z)
409        let time_of_request_to_fetch_credentials = UNIX_EPOCH + Duration::from_secs(1632246085);
410        let (time_source, sleep) = instant_time_and_sleep(time_of_request_to_fetch_credentials);
411
412        let provider_config = ProviderConfig::no_configuration()
413            .with_http_client(http_client.clone())
414            .with_sleep_impl(sleep)
415            .with_time_source(time_source);
416        let client = crate::imds::Client::builder()
417            .configure(&provider_config)
418            .build();
419        let provider = ImdsCredentialsProvider::builder()
420            .configure(&provider_config)
421            .imds_client(client)
422            .build();
423        let creds = provider.provide_credentials().await.expect("valid creds");
424        assert!(creds.expiry().unwrap() > time_of_request_to_fetch_credentials);
425        http_client.assert_requests_match(&[]);
426
427        // We should inform customers that expired credentials are being used for stability.
428        assert!(logs_contain(WARNING_FOR_EXTENDING_CREDENTIALS_EXPIRY));
429    }
430
431    #[tokio::test]
432    #[cfg(feature = "default-https-client")]
433    async fn read_timeout_during_credentials_refresh_should_yield_last_retrieved_credentials() {
434        let client = crate::imds::Client::builder()
435            // 240.* can never be resolved
436            .endpoint("http://240.0.0.0")
437            .unwrap()
438            .build();
439        let expected = aws_credential_types::Credentials::for_tests();
440        let provider = ImdsCredentialsProvider::builder()
441            .imds_client(client)
442            // seed fallback credentials for testing
443            .last_retrieved_credentials(expected.clone())
444            .build();
445        let actual = provider.provide_credentials().await;
446        assert_eq!(actual.unwrap(), expected);
447    }
448
449    #[tokio::test]
450    #[cfg(feature = "default-https-client")]
451    async fn read_timeout_during_credentials_refresh_should_error_without_last_retrieved_credentials(
452    ) {
453        let client = crate::imds::Client::builder()
454            // 240.* can never be resolved
455            .endpoint("http://240.0.0.0")
456            .unwrap()
457            .build();
458        let provider = ImdsCredentialsProvider::builder()
459            .imds_client(client)
460            // no fallback credentials provided
461            .build();
462        let actual = provider.provide_credentials().await;
463        assert!(
464            matches!(actual, Err(CredentialsError::CredentialsNotLoaded(_))),
465            "\nexpected: Err(CredentialsError::CredentialsNotLoaded(_))\nactual: {actual:?}"
466        );
467    }
468
469    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is ignored on Windows because it uses Unix-style paths
470    #[cfg_attr(windows, ignore)]
471    #[tokio::test]
472    #[cfg(feature = "default-https-client")]
473    async fn external_timeout_during_credentials_refresh_should_yield_last_retrieved_credentials() {
474        use aws_smithy_async::rt::sleep::AsyncSleep;
475        let client = crate::imds::Client::builder()
476            // 240.* can never be resolved
477            .endpoint("http://240.0.0.0")
478            .unwrap()
479            .build();
480        let expected = aws_credential_types::Credentials::for_tests();
481        let provider = ImdsCredentialsProvider::builder()
482            .imds_client(client)
483            .configure(&ProviderConfig::no_configuration())
484            // seed fallback credentials for testing
485            .last_retrieved_credentials(expected.clone())
486            .build();
487        let sleeper = aws_smithy_async::rt::sleep::TokioSleep::new();
488        let timeout = aws_smithy_async::future::timeout::Timeout::new(
489            provider.provide_credentials(),
490            // make sure `sleeper.sleep` will be timed out first by setting a shorter duration than connect timeout
491            sleeper.sleep(std::time::Duration::from_millis(100)),
492        );
493        match timeout.await {
494            Ok(_) => panic!("provide_credentials completed before timeout future"),
495            Err(_err) => match provider.fallback_on_interrupt() {
496                Some(actual) => assert_eq!(actual, expected),
497                None => panic!(
498                    "provide_credentials timed out and no credentials returned from fallback_on_interrupt"
499                ),
500            },
501        };
502    }
503
504    #[tokio::test]
505    async fn fallback_credentials_should_be_used_when_imds_returns_500_during_credentials_refresh()
506    {
507        let http_client = StaticReplayClient::new(vec![
508                // The next three request/response pairs will correspond to the first call to `provide_credentials`.
509                // During the call, it populates last_retrieved_credentials.
510                ReplayEvent::new(
511                    token_request("http://169.254.169.254", 21600),
512                    token_response(21600, TOKEN_A),
513                ),
514                ReplayEvent::new(
515                    imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
516                    imds_response(r#"profile-name"#),
517                ),
518                ReplayEvent::new(
519                    imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
520                    imds_response("{\n  \"Code\" : \"Success\",\n  \"LastUpdated\" : \"2021-09-20T21:42:26Z\",\n  \"Type\" : \"AWS-HMAC\",\n  \"AccessKeyId\" : \"ASIARTEST\",\n  \"SecretAccessKey\" : \"testsecret\",\n  \"Token\" : \"testtoken\",\n  \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}"),
521                ),
522                // The following request/response pair corresponds to the second call to `provide_credentials`.
523                // During the call, IMDS returns response code 500.
524                ReplayEvent::new(
525                    imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
526                    http::Response::builder().status(500).body(SdkBody::empty()).unwrap(),
527                ),
528            ]);
529        let provider = ImdsCredentialsProvider::builder()
530            .imds_client(make_imds_client(&http_client))
531            .configure(&ProviderConfig::no_configuration())
532            .build();
533        let creds1 = provider.provide_credentials().await.expect("valid creds");
534        assert_eq!(creds1.access_key_id(), "ASIARTEST");
535        // `creds1` should be returned as fallback credentials and assigned to `creds2`
536        let creds2 = provider.provide_credentials().await.expect("valid creds");
537        assert_eq!(creds1, creds2);
538        http_client.assert_requests_match(&[]);
539    }
540
541    #[tokio::test]
542    async fn credentials_feature() {
543        let http_client = StaticReplayClient::new(vec![
544            ReplayEvent::new(
545                token_request("http://169.254.169.254", 21600),
546                token_response(21600, TOKEN_A),
547            ),
548            ReplayEvent::new(
549                imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
550                imds_response(r#"profile-name"#),
551            ),
552            ReplayEvent::new(
553                imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
554                imds_response("{\n  \"Code\" : \"Success\",\n  \"LastUpdated\" : \"2021-09-20T21:42:26Z\",\n  \"Type\" : \"AWS-HMAC\",\n  \"AccessKeyId\" : \"ASIARTEST\",\n  \"SecretAccessKey\" : \"testsecret\",\n  \"Token\" : \"testtoken\",\n  \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}"),
555            ),
556            ReplayEvent::new(
557                imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
558                imds_response(r#"different-profile"#),
559            ),
560            ReplayEvent::new(
561                imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/different-profile", TOKEN_A),
562                imds_response("{\n  \"Code\" : \"Success\",\n  \"LastUpdated\" : \"2021-09-20T21:42:26Z\",\n  \"Type\" : \"AWS-HMAC\",\n  \"AccessKeyId\" : \"ASIARTEST2\",\n  \"SecretAccessKey\" : \"testsecret\",\n  \"Token\" : \"testtoken\",\n  \"Expiration\" : \"2021-09-21T04:16:53Z\"\n}"),
563            ),
564        ]);
565        let client = ImdsCredentialsProvider::builder()
566            .imds_client(make_imds_client(&http_client))
567            .configure(&ProviderConfig::no_configuration())
568            .build();
569        let creds = client.provide_credentials().await.expect("valid creds");
570        assert_eq!(
571            &vec![AwsCredentialFeature::CredentialsImds],
572            creds.get_property::<Vec<AwsCredentialFeature>>().unwrap()
573        );
574    }
575}