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/{profile}",
229            ))
230            .await
231            .map_err(CredentialsError::provider_error)?;
232        match parse_json_credentials(credentials.as_ref()) {
233            Ok(JsonCredentials::RefreshableCredentials(RefreshableCredentials {
234                access_key_id,
235                secret_access_key,
236                session_token,
237                account_id,
238                expiration,
239                ..
240            })) => {
241                // TODO(IMDSv2.X): Use `account_id` once the design is finalized
242                let _ = account_id;
243                let expiration = self.maybe_extend_expiration(expiration);
244                let creds = Credentials::new(
245                    access_key_id,
246                    secret_access_key,
247                    Some(session_token.to_string()),
248                    expiration.into(),
249                    "IMDSv2",
250                );
251                *self.last_retrieved_credentials.write().unwrap() = Some(creds.clone());
252                Ok(creds)
253            }
254            Ok(JsonCredentials::Error { code, message })
255                if code == codes::ASSUME_ROLE_UNAUTHORIZED_ACCESS =>
256            {
257                Err(CredentialsError::invalid_configuration(format!(
258                    "Incorrect IMDS/IAM configuration: [{code}] {message}. \
259                        Hint: Does this role have a trust relationship with EC2?",
260                )))
261            }
262            Ok(JsonCredentials::Error { code, message }) => Err(CredentialsError::provider_error(
263                format!("Error retrieving credentials from IMDS: {code} {message}"),
264            )),
265            // got bad data from IMDS, should not occur during normal operation:
266            Err(invalid) => Err(CredentialsError::unhandled(invalid)),
267        }
268        .map(|mut creds| {
269            creds
270                .get_property_mut_or_default::<Vec<AwsCredentialFeature>>()
271                .push(AwsCredentialFeature::CredentialsImds);
272            creds
273        })
274    }
275
276    async fn credentials(&self) -> provider::Result {
277        match self.retrieve_credentials().await {
278            creds @ Ok(_) => creds,
279            // Any failure while retrieving credentials MUST NOT impede use of existing credentials.
280            err => match &*self.last_retrieved_credentials.read().unwrap() {
281                Some(creds) => Ok(creds.clone()),
282                _ => err,
283            },
284        }
285    }
286}
287
288#[cfg(test)]
289mod test {
290    use super::*;
291    use crate::imds::client::test::{
292        imds_request, imds_response, make_imds_client, token_request, token_response,
293    };
294    use crate::provider_config::ProviderConfig;
295    use aws_credential_types::credential_feature::AwsCredentialFeature;
296    use aws_credential_types::provider::ProvideCredentials;
297    use aws_smithy_async::test_util::instant_time_and_sleep;
298    use aws_smithy_http_client::test_util::{ReplayEvent, StaticReplayClient};
299    use aws_smithy_types::body::SdkBody;
300    use std::time::{Duration, UNIX_EPOCH};
301    use tracing_test::traced_test;
302
303    const TOKEN_A: &str = "token_a";
304
305    #[tokio::test]
306    async fn profile_is_not_cached() {
307        let http_client = StaticReplayClient::new(vec![
308            ReplayEvent::new(
309                token_request("http://169.254.169.254", 21600),
310                token_response(21600, TOKEN_A),
311            ),
312            ReplayEvent::new(
313                imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
314                imds_response(r#"profile-name"#),
315            ),
316            ReplayEvent::new(
317                imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
318                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}"),
319            ),
320            ReplayEvent::new(
321                imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
322                imds_response(r#"different-profile"#),
323            ),
324            ReplayEvent::new(
325                imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/different-profile", TOKEN_A),
326                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}"),
327            ),
328        ]);
329        let client = ImdsCredentialsProvider::builder()
330            .imds_client(make_imds_client(&http_client))
331            .configure(&ProviderConfig::no_configuration())
332            .build();
333        let creds1 = client.provide_credentials().await.expect("valid creds");
334        let creds2 = client.provide_credentials().await.expect("valid creds");
335        assert_eq!(creds1.access_key_id(), "ASIARTEST");
336        assert_eq!(creds2.access_key_id(), "ASIARTEST2");
337        http_client.assert_requests_match(&[]);
338    }
339
340    #[tokio::test]
341    #[traced_test]
342    async fn credentials_not_stale_should_be_used_as_they_are() {
343        let http_client = StaticReplayClient::new(vec![
344            ReplayEvent::new(
345                token_request("http://169.254.169.254", 21600),
346                token_response(21600, TOKEN_A),
347            ),
348            ReplayEvent::new(
349                imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
350                imds_response(r#"profile-name"#),
351            ),
352            ReplayEvent::new(
353                imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
354                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}"),
355            ),
356        ]);
357
358        // set to 2021-09-21T04:16:50Z that makes returned credentials' expiry (2021-09-21T04:16:53Z)
359        // not stale
360        let time_of_request_to_fetch_credentials = UNIX_EPOCH + Duration::from_secs(1632197810);
361        let (time_source, sleep) = instant_time_and_sleep(time_of_request_to_fetch_credentials);
362
363        let provider_config = ProviderConfig::no_configuration()
364            .with_http_client(http_client.clone())
365            .with_sleep_impl(sleep)
366            .with_time_source(time_source);
367        let client = crate::imds::Client::builder()
368            .configure(&provider_config)
369            .build();
370        let provider = ImdsCredentialsProvider::builder()
371            .configure(&provider_config)
372            .imds_client(client)
373            .build();
374        let creds = provider.provide_credentials().await.expect("valid creds");
375        // The expiry should be equal to what is originally set (==2021-09-21T04:16:53Z).
376        assert_eq!(
377            creds.expiry(),
378            UNIX_EPOCH.checked_add(Duration::from_secs(1632197813))
379        );
380        http_client.assert_requests_match(&[]);
381
382        // There should not be logs indicating credentials are extended for stability.
383        assert!(!logs_contain(WARNING_FOR_EXTENDING_CREDENTIALS_EXPIRY));
384    }
385    #[tokio::test]
386    #[traced_test]
387    async fn expired_credentials_should_be_extended() {
388        let http_client = StaticReplayClient::new(vec![
389                ReplayEvent::new(
390                    token_request("http://169.254.169.254", 21600),
391                    token_response(21600, TOKEN_A),
392                ),
393                ReplayEvent::new(
394                    imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
395                    imds_response(r#"profile-name"#),
396                ),
397                ReplayEvent::new(
398                    imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
399                    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}"),
400                ),
401            ]);
402
403        // set to 2021-09-21T17:41:25Z that renders fetched credentials already expired (2021-09-21T04:16:53Z)
404        let time_of_request_to_fetch_credentials = UNIX_EPOCH + Duration::from_secs(1632246085);
405        let (time_source, sleep) = instant_time_and_sleep(time_of_request_to_fetch_credentials);
406
407        let provider_config = ProviderConfig::no_configuration()
408            .with_http_client(http_client.clone())
409            .with_sleep_impl(sleep)
410            .with_time_source(time_source);
411        let client = crate::imds::Client::builder()
412            .configure(&provider_config)
413            .build();
414        let provider = ImdsCredentialsProvider::builder()
415            .configure(&provider_config)
416            .imds_client(client)
417            .build();
418        let creds = provider.provide_credentials().await.expect("valid creds");
419        assert!(creds.expiry().unwrap() > time_of_request_to_fetch_credentials);
420        http_client.assert_requests_match(&[]);
421
422        // We should inform customers that expired credentials are being used for stability.
423        assert!(logs_contain(WARNING_FOR_EXTENDING_CREDENTIALS_EXPIRY));
424    }
425
426    #[tokio::test]
427    #[cfg(feature = "default-https-client")]
428    async fn read_timeout_during_credentials_refresh_should_yield_last_retrieved_credentials() {
429        let client = crate::imds::Client::builder()
430            // 240.* can never be resolved
431            .endpoint("http://240.0.0.0")
432            .unwrap()
433            .build();
434        let expected = aws_credential_types::Credentials::for_tests();
435        let provider = ImdsCredentialsProvider::builder()
436            .imds_client(client)
437            // seed fallback credentials for testing
438            .last_retrieved_credentials(expected.clone())
439            .build();
440        let actual = provider.provide_credentials().await;
441        assert_eq!(actual.unwrap(), expected);
442    }
443
444    #[tokio::test]
445    #[cfg(feature = "default-https-client")]
446    async fn read_timeout_during_credentials_refresh_should_error_without_last_retrieved_credentials(
447    ) {
448        let client = crate::imds::Client::builder()
449            // 240.* can never be resolved
450            .endpoint("http://240.0.0.0")
451            .unwrap()
452            .build();
453        let provider = ImdsCredentialsProvider::builder()
454            .imds_client(client)
455            // no fallback credentials provided
456            .build();
457        let actual = provider.provide_credentials().await;
458        assert!(
459            matches!(actual, Err(CredentialsError::CredentialsNotLoaded(_))),
460            "\nexpected: Err(CredentialsError::CredentialsNotLoaded(_))\nactual: {actual:?}"
461        );
462    }
463
464    // TODO(https://github.com/awslabs/aws-sdk-rust/issues/1117) This test is ignored on Windows because it uses Unix-style paths
465    #[cfg_attr(windows, ignore)]
466    #[tokio::test]
467    #[cfg(feature = "default-https-client")]
468    async fn external_timeout_during_credentials_refresh_should_yield_last_retrieved_credentials() {
469        use aws_smithy_async::rt::sleep::AsyncSleep;
470        let client = crate::imds::Client::builder()
471            // 240.* can never be resolved
472            .endpoint("http://240.0.0.0")
473            .unwrap()
474            .build();
475        let expected = aws_credential_types::Credentials::for_tests();
476        let provider = ImdsCredentialsProvider::builder()
477            .imds_client(client)
478            .configure(&ProviderConfig::no_configuration())
479            // seed fallback credentials for testing
480            .last_retrieved_credentials(expected.clone())
481            .build();
482        let sleeper = aws_smithy_async::rt::sleep::TokioSleep::new();
483        let timeout = aws_smithy_async::future::timeout::Timeout::new(
484            provider.provide_credentials(),
485            // make sure `sleeper.sleep` will be timed out first by setting a shorter duration than connect timeout
486            sleeper.sleep(std::time::Duration::from_millis(100)),
487        );
488        match timeout.await {
489            Ok(_) => panic!("provide_credentials completed before timeout future"),
490            Err(_err) => match provider.fallback_on_interrupt() {
491                Some(actual) => assert_eq!(actual, expected),
492                None => panic!(
493                    "provide_credentials timed out and no credentials returned from fallback_on_interrupt"
494                ),
495            },
496        };
497    }
498
499    #[tokio::test]
500    async fn fallback_credentials_should_be_used_when_imds_returns_500_during_credentials_refresh()
501    {
502        let http_client = StaticReplayClient::new(vec![
503                // The next three request/response pairs will correspond to the first call to `provide_credentials`.
504                // During the call, it populates last_retrieved_credentials.
505                ReplayEvent::new(
506                    token_request("http://169.254.169.254", 21600),
507                    token_response(21600, TOKEN_A),
508                ),
509                ReplayEvent::new(
510                    imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
511                    imds_response(r#"profile-name"#),
512                ),
513                ReplayEvent::new(
514                    imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
515                    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}"),
516                ),
517                // The following request/response pair corresponds to the second call to `provide_credentials`.
518                // During the call, IMDS returns response code 500.
519                ReplayEvent::new(
520                    imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
521                    http::Response::builder().status(500).body(SdkBody::empty()).unwrap(),
522                ),
523            ]);
524        let provider = ImdsCredentialsProvider::builder()
525            .imds_client(make_imds_client(&http_client))
526            .configure(&ProviderConfig::no_configuration())
527            .build();
528        let creds1 = provider.provide_credentials().await.expect("valid creds");
529        assert_eq!(creds1.access_key_id(), "ASIARTEST");
530        // `creds1` should be returned as fallback credentials and assigned to `creds2`
531        let creds2 = provider.provide_credentials().await.expect("valid creds");
532        assert_eq!(creds1, creds2);
533        http_client.assert_requests_match(&[]);
534    }
535
536    #[tokio::test]
537    async fn credentials_feature() {
538        let http_client = StaticReplayClient::new(vec![
539            ReplayEvent::new(
540                token_request("http://169.254.169.254", 21600),
541                token_response(21600, TOKEN_A),
542            ),
543            ReplayEvent::new(
544                imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
545                imds_response(r#"profile-name"#),
546            ),
547            ReplayEvent::new(
548                imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/profile-name", TOKEN_A),
549                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}"),
550            ),
551            ReplayEvent::new(
552                imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", TOKEN_A),
553                imds_response(r#"different-profile"#),
554            ),
555            ReplayEvent::new(
556                imds_request("http://169.254.169.254/latest/meta-data/iam/security-credentials/different-profile", TOKEN_A),
557                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}"),
558            ),
559        ]);
560        let client = ImdsCredentialsProvider::builder()
561            .imds_client(make_imds_client(&http_client))
562            .configure(&ProviderConfig::no_configuration())
563            .build();
564        let creds = client.provide_credentials().await.expect("valid creds");
565        assert_eq!(
566            &vec![AwsCredentialFeature::CredentialsImds],
567            creds.get_property::<Vec<AwsCredentialFeature>>().unwrap()
568        );
569    }
570}