1use crate::identity::IdentityCache;
14use crate::sso::cache::{
15 load_cached_token, save_cached_token, CachedSsoToken, CachedSsoTokenError,
16};
17use aws_credential_types::provider::token::ProvideToken;
18use aws_credential_types::provider::{
19 error::TokenError, future::ProvideToken as ProvideTokenFuture,
20};
21use aws_sdk_ssooidc::error::DisplayErrorContext;
22use aws_sdk_ssooidc::operation::create_token::CreateTokenOutput;
23use aws_sdk_ssooidc::Client as SsoOidcClient;
24use aws_smithy_async::time::SharedTimeSource;
25use aws_smithy_runtime::expiring_cache::ExpiringCache;
26use aws_smithy_runtime_api::client::identity::http::Token;
27use aws_smithy_runtime_api::client::identity::{IdentityFuture, ResolveIdentity};
28use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
29use aws_smithy_types::config_bag::ConfigBag;
30use aws_types::os_shim_internal::{Env, Fs};
31use aws_types::region::Region;
32use aws_types::SdkConfig;
33use std::error::Error as StdError;
34use std::fmt;
35use std::sync::{Arc, Mutex};
36use std::time::{Duration, SystemTime};
37use zeroize::Zeroizing;
38
39const REFRESH_BUFFER_TIME: Duration = Duration::from_secs(5 * 60 );
40const MIN_TIME_BETWEEN_REFRESH: Duration = Duration::from_secs(30);
41
42#[derive(Debug)]
49pub struct SsoTokenProvider {
50 inner: Arc<Inner>,
51 token_cache: ExpiringCache<CachedSsoToken, SsoTokenProviderError>,
52}
53
54#[derive(Debug)]
55struct Inner {
56 env: Env,
57 fs: Fs,
58 region: Region,
59 session_name: String,
60 start_url: String,
61 sdk_config: SdkConfig,
62 last_refresh_attempt: Mutex<Option<SystemTime>>,
63}
64
65impl SsoTokenProvider {
66 pub fn builder() -> Builder {
68 Default::default()
69 }
70
71 async fn refresh_cached_token(
72 inner: &Inner,
73 cached_token: &CachedSsoToken,
74 identifier: &str,
75 now: SystemTime,
76 ) -> Result<Option<CachedSsoToken>, SsoTokenProviderError> {
77 let config = inner
79 .sdk_config
80 .to_builder()
81 .region(Some(inner.region.clone()))
82 .identity_cache(IdentityCache::no_cache())
83 .build();
84 let client = SsoOidcClient::new(&config);
85 let resp = client
86 .create_token()
87 .grant_type("refresh_token")
88 .client_id(
89 cached_token
90 .client_id
91 .as_ref()
92 .expect("required for token refresh")
93 .clone(),
94 )
95 .client_secret(
96 cached_token
97 .client_secret
98 .as_ref()
99 .expect("required for token refresh")
100 .as_str(),
101 )
102 .refresh_token(
103 cached_token
104 .refresh_token
105 .as_ref()
106 .expect("required for token refresh")
107 .as_str(),
108 )
109 .send()
110 .await;
111 match resp {
112 Ok(CreateTokenOutput {
113 access_token: Some(access_token),
114 refresh_token,
115 expires_in,
116 ..
117 }) => {
118 let refreshed_token = CachedSsoToken {
119 access_token: Zeroizing::new(access_token),
120 client_id: cached_token.client_id.clone(),
121 client_secret: cached_token.client_secret.clone(),
122 expires_at: now
123 + Duration::from_secs(
124 u64::try_from(expires_in)
125 .map_err(|_| SsoTokenProviderError::BadExpirationTimeFromSsoOidc)?,
126 ),
127 refresh_token: refresh_token
128 .map(Zeroizing::new)
129 .or_else(|| cached_token.refresh_token.clone()),
130 region: Some(inner.region.to_string()),
131 registration_expires_at: cached_token.registration_expires_at,
132 start_url: Some(inner.start_url.clone()),
133 };
134 save_cached_token(&inner.env, &inner.fs, identifier, &refreshed_token).await?;
135 tracing::debug!("saved refreshed SSO token");
136 Ok(Some(refreshed_token))
137 }
138 Ok(_) => {
139 tracing::debug!("SSO OIDC CreateToken responded without an access token");
140 Ok(None)
141 }
142 Err(err) => {
143 tracing::debug!(
144 "call to SSO OIDC CreateToken for SSO token refresh failed: {}",
145 DisplayErrorContext(&err)
146 );
147 Ok(None)
148 }
149 }
150 }
151
152 pub(super) fn resolve_token(
153 &self,
154 time_source: SharedTimeSource,
155 ) -> impl std::future::Future<Output = Result<CachedSsoToken, TokenError>> + 'static {
156 let token_cache = self.token_cache.clone();
157 let inner = self.inner.clone();
158
159 async move {
160 if let Some(token) = token_cache
161 .yield_or_clear_if_expired(time_source.now())
162 .await
163 {
164 tracing::debug!("using cached SSO token");
165 return Ok(token);
166 }
167 let token = token_cache
168 .get_or_load(|| async move {
169 tracing::debug!("expiring cache asked for an updated SSO token");
170 let mut token =
171 load_cached_token(&inner.env, &inner.fs, &inner.session_name).await?;
172 tracing::debug!("loaded cached SSO token");
173
174 let now = time_source.now();
175 let expired = token.expires_at <= now;
176 let expires_soon = token.expires_at - REFRESH_BUFFER_TIME <= now;
177 let last_refresh = *inner.last_refresh_attempt.lock().unwrap();
178 let min_time_passed = last_refresh
179 .map(|lr| {
180 now.duration_since(lr).expect("last_refresh is in the past")
181 >= MIN_TIME_BETWEEN_REFRESH
182 })
183 .unwrap_or(true);
184 let registration_expired = token
185 .registration_expires_at
186 .map(|t| t <= now)
187 .unwrap_or(true);
188 let refreshable =
189 token.refreshable() && min_time_passed && !registration_expired;
190
191 tracing::debug!(
192 expired = ?expired,
193 expires_soon = ?expires_soon,
194 min_time_passed = ?min_time_passed,
195 registration_expired = ?registration_expired,
196 refreshable = ?refreshable,
197 will_refresh = ?(expires_soon && refreshable),
198 "cached SSO token refresh decision"
199 );
200
201 if expired && !refreshable {
203 tracing::debug!("cached SSO token is expired and cannot be refreshed");
204 return Err(SsoTokenProviderError::ExpiredToken);
205 }
206
207 if expires_soon && refreshable {
209 tracing::debug!("attempting to refresh SSO token");
210 if let Some(refreshed_token) =
211 Self::refresh_cached_token(&inner, &token, &inner.session_name, now)
212 .await?
213 {
214 token = refreshed_token;
215 }
216 *inner.last_refresh_attempt.lock().unwrap() = Some(now);
217 }
218
219 let expires_at = token.expires_at;
220 Ok((token, expires_at))
221 })
222 .await
223 .map_err(TokenError::provider_error)?;
224
225 Ok(token)
226 }
227 }
228}
229
230impl ProvideToken for SsoTokenProvider {
231 fn provide_token<'a>(&'a self) -> ProvideTokenFuture<'a>
232 where
233 Self: 'a,
234 {
235 let time_source = self
236 .inner
237 .sdk_config
238 .time_source()
239 .expect("a time source required by SsoTokenProvider");
240 let token_future = self.resolve_token(time_source);
241 ProvideTokenFuture::new(Box::pin(async move {
242 let token = token_future.await?;
243 Ok(Token::new(
244 token.access_token.as_str(),
245 Some(token.expires_at),
246 ))
247 }))
248 }
249}
250
251impl ResolveIdentity for SsoTokenProvider {
252 fn resolve_identity<'a>(
253 &'a self,
254 runtime_components: &'a RuntimeComponents,
255 config_bag: &'a ConfigBag,
256 ) -> IdentityFuture<'a> {
257 IdentityFuture::new(Box::pin(async move {
258 self.provide_token()
259 .await?
260 .resolve_identity(runtime_components, config_bag)
261 .await
262 }))
263 }
264}
265
266#[derive(Debug, Default)]
268pub struct Builder {
269 sdk_config: Option<SdkConfig>,
270 region: Option<Region>,
271 session_name: Option<String>,
272 start_url: Option<String>,
273}
274
275impl Builder {
276 pub fn new() -> Self {
278 Default::default()
279 }
280
281 pub fn configure(mut self, sdk_config: &SdkConfig) -> Self {
283 self.sdk_config = Some(sdk_config.clone());
284 self
285 }
286
287 pub fn region(mut self, region: impl Into<Region>) -> Self {
291 self.region = Some(region.into());
292 self
293 }
294
295 pub fn set_region(&mut self, region: Option<Region>) -> &mut Self {
299 self.region = region;
300 self
301 }
302
303 pub fn session_name(mut self, session_name: impl Into<String>) -> Self {
307 self.session_name = Some(session_name.into());
308 self
309 }
310
311 pub fn set_session_name(&mut self, session_name: Option<String>) -> &mut Self {
315 self.session_name = session_name;
316 self
317 }
318
319 pub fn start_url(mut self, start_url: impl Into<String>) -> Self {
323 self.start_url = Some(start_url.into());
324 self
325 }
326
327 pub fn set_start_url(&mut self, start_url: Option<String>) -> &mut Self {
331 self.start_url = start_url;
332 self
333 }
334
335 pub async fn build(mut self) -> SsoTokenProvider {
341 if self.sdk_config.is_none() {
342 self.sdk_config = Some(crate::load_defaults(crate::BehaviorVersion::latest()).await);
343 }
344 self.build_with(Env::real(), Fs::real())
345 }
346
347 pub(crate) fn build_with(self, env: Env, fs: Fs) -> SsoTokenProvider {
348 SsoTokenProvider {
349 inner: Arc::new(Inner {
350 env,
351 fs,
352 region: self.region.expect("region is required"),
353 session_name: self.session_name.expect("session_name is required"),
354 start_url: self.start_url.expect("start_url is required"),
355 sdk_config: self.sdk_config.expect("sdk_config is required"),
356 last_refresh_attempt: Mutex::new(None),
357 }),
358 token_cache: ExpiringCache::new(REFRESH_BUFFER_TIME),
359 }
360 }
361}
362
363#[derive(Debug)]
364pub(super) enum SsoTokenProviderError {
365 BadExpirationTimeFromSsoOidc,
366 FailedToLoadToken {
367 source: Box<dyn StdError + Send + Sync>,
368 },
369 ExpiredToken,
370}
371
372impl fmt::Display for SsoTokenProviderError {
373 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
374 match self {
375 Self::BadExpirationTimeFromSsoOidc => {
376 f.write_str("SSO OIDC responded with a negative expiration duration")
377 }
378 Self::ExpiredToken => f.write_str("the SSO token has expired and cannot be refreshed"),
379 Self::FailedToLoadToken { .. } => f.write_str("failed to load the cached SSO token"),
380 }
381 }
382}
383
384impl StdError for SsoTokenProviderError {
385 fn cause(&self) -> Option<&dyn StdError> {
386 match self {
387 Self::BadExpirationTimeFromSsoOidc => None,
388 Self::ExpiredToken => None,
389 Self::FailedToLoadToken { source } => Some(source.as_ref()),
390 }
391 }
392}
393
394impl From<CachedSsoTokenError> for SsoTokenProviderError {
395 fn from(source: CachedSsoTokenError) -> Self {
396 Self::FailedToLoadToken {
397 source: source.into(),
398 }
399 }
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405 use aws_sdk_sso::config::{AsyncSleep, SharedAsyncSleep};
406 use aws_smithy_async::rt::sleep::TokioSleep;
407 use aws_smithy_async::test_util::instant_time_and_sleep;
408 use aws_smithy_async::time::{StaticTimeSource, TimeSource};
409 use aws_smithy_runtime::test_util::capture_test_logs::capture_test_logs;
410 use aws_smithy_runtime::{
411 assert_str_contains,
412 client::http::test_util::{capture_request, ReplayEvent, StaticReplayClient},
413 };
414 use aws_smithy_runtime_api::client::http::HttpClient;
415 use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
416 use aws_smithy_types::body::SdkBody;
417 use aws_smithy_types::date_time::Format;
418 use aws_smithy_types::retry::RetryConfig;
419 use aws_smithy_types::DateTime;
420
421 fn time(s: &str) -> SystemTime {
422 SystemTime::try_from(DateTime::from_str(s, Format::DateTime).unwrap()).unwrap()
423 }
424
425 struct TestHarness {
426 time_source: SharedTimeSource,
427 token_provider: SsoTokenProvider,
428 env: Env,
429 fs: Fs,
430 }
431
432 impl TestHarness {
433 fn new(
434 time_source: impl TimeSource + 'static,
435 sleep_impl: impl AsyncSleep + 'static,
436 http_client: impl HttpClient + 'static,
437 fs: Fs,
438 ) -> Self {
439 let env = Env::from_slice(&[("HOME", "/home/user")]);
440 let time_source = SharedTimeSource::new(time_source);
441 let config = SdkConfig::builder()
442 .http_client(http_client)
443 .time_source(time_source.clone())
444 .sleep_impl(SharedAsyncSleep::new(sleep_impl))
445 .retry_config(RetryConfig::disabled())
447 .behavior_version(crate::BehaviorVersion::latest())
448 .build();
449 Self {
450 time_source,
451 token_provider: SsoTokenProvider::builder()
452 .configure(&config)
453 .session_name("test")
454 .region(Region::new("us-west-2"))
455 .start_url("https://d-123.awsapps.com/start")
456 .build_with(env.clone(), fs.clone()),
457 env,
458 fs,
459 }
460 }
461
462 async fn expect_sso_token(&self, value: &str, expires_at: &str) -> CachedSsoToken {
463 let token = self
464 .token_provider
465 .resolve_token(self.time_source.clone())
466 .await
467 .unwrap();
468 assert_eq!(value, token.access_token.as_str());
469 assert_eq!(time(expires_at), token.expires_at);
470 token
471 }
472
473 async fn expect_token(&self, value: &str, expires_at: &str) {
474 let runtime_components = RuntimeComponentsBuilder::for_tests()
475 .with_time_source(Some(self.time_source.clone()))
476 .build()
477 .unwrap();
478 let config_bag = ConfigBag::base();
479 let identity = self
480 .token_provider
481 .resolve_identity(&runtime_components, &config_bag)
482 .await
483 .unwrap();
484 let token = identity.data::<Token>().unwrap().clone();
485 assert_eq!(value, token.token());
486 assert_eq!(time(expires_at), identity.expiration().unwrap());
487 }
488
489 async fn expect_expired_token_err(&self) {
490 let err = DisplayErrorContext(
491 &self
492 .token_provider
493 .resolve_token(self.time_source.clone())
494 .await
495 .expect_err("expected failure"),
496 )
497 .to_string();
498 assert_str_contains!(err, "the SSO token has expired");
499 }
500
501 fn last_refresh_attempt_time(&self) -> Option<String> {
502 self.token_provider
503 .inner
504 .last_refresh_attempt
505 .lock()
506 .unwrap()
507 .map(|time| {
508 DateTime::try_from(time)
509 .unwrap()
510 .fmt(Format::DateTime)
511 .unwrap()
512 })
513 }
514 }
515
516 #[cfg_attr(windows, ignore)]
518 #[tokio::test]
519 async fn use_unexpired_cached_token() {
520 let fs = Fs::from_slice(&[(
521 "/home/user/.aws/sso/cache/a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.json",
522 r#"
523 { "accessToken": "some-token",
524 "expiresAt": "1975-01-01T00:00:00Z" }
525 "#,
526 )]);
527
528 let now = time("1974-12-25T00:00:00Z");
529 let time_source = SharedTimeSource::new(StaticTimeSource::new(now));
530
531 let (conn, req_rx) = capture_request(None);
532 let harness = TestHarness::new(time_source, TokioSleep::new(), conn, fs);
533
534 harness
535 .expect_token("some-token", "1975-01-01T00:00:00Z")
536 .await;
537 req_rx.expect_no_request();
539 }
540
541 #[cfg_attr(windows, ignore)]
543 #[tokio::test]
544 async fn expired_cached_token() {
545 let fs = Fs::from_slice(&[(
546 "/home/user/.aws/sso/cache/a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.json",
547 r#"
548 { "accessToken": "some-token",
549 "expiresAt": "1999-12-15T00:00:00Z" }
550 "#,
551 )]);
552
553 let now = time("2023-01-01T00:00:00Z");
554 let time_source = SharedTimeSource::new(StaticTimeSource::new(now));
555
556 let (conn, req_rx) = capture_request(None);
557 let harness = TestHarness::new(time_source, TokioSleep::new(), conn, fs);
558
559 harness.expect_expired_token_err().await;
560 req_rx.expect_no_request();
562 }
563
564 #[cfg_attr(windows, ignore)]
566 #[tokio::test]
567 async fn expired_token_and_expired_client_registration() {
568 let fs = Fs::from_slice(&[(
569 "/home/user/.aws/sso/cache/a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.json",
570 r#"
571 { "startUrl": "https://d-123.awsapps.com/start",
572 "region": "us-west-2",
573 "accessToken": "cachedtoken",
574 "expiresAt": "2021-10-25T13:00:00Z",
575 "clientId": "clientid",
576 "clientSecret": "YSBzZWNyZXQ=",
577 "registrationExpiresAt": "2021-11-25T13:30:00Z",
578 "refreshToken": "cachedrefreshtoken" }
579 "#,
580 )]);
581
582 let now = time("2023-08-11T04:11:17Z");
583 let time_source = SharedTimeSource::new(StaticTimeSource::new(now));
584
585 let (conn, req_rx) = capture_request(None);
586 let harness = TestHarness::new(time_source, TokioSleep::new(), conn, fs);
587
588 harness.expect_expired_token_err().await;
590 req_rx.expect_no_request();
591 }
592
593 #[cfg_attr(windows, ignore)]
595 #[tokio::test]
596 async fn expired_token_refresh_with_refresh_token() {
597 let fs = Fs::from_slice(&[(
598 "/home/user/.aws/sso/cache/a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.json",
599 r#"
600 { "startUrl": "https://d-123.awsapps.com/start",
601 "region": "us-west-2",
602 "accessToken": "cachedtoken",
603 "expiresAt": "2021-12-25T13:00:00Z",
604 "clientId": "clientid",
605 "clientSecret": "YSBzZWNyZXQ=",
606 "registrationExpiresAt": "2022-12-25T13:30:00Z",
607 "refreshToken": "cachedrefreshtoken" }
608 "#,
609 )]);
610
611 let now = time("2021-12-25T13:30:00Z");
612 let time_source = SharedTimeSource::new(StaticTimeSource::new(now));
613
614 let (conn, req_rx) = capture_request(Some(
615 http::Response::builder()
616 .status(200)
617 .body(SdkBody::from(
618 r#"
619 { "tokenType": "Bearer",
620 "accessToken": "newtoken",
621 "expiresIn": 28800,
622 "refreshToken": "newrefreshtoken" }
623 "#,
624 ))
625 .unwrap(),
626 ));
627 let harness = TestHarness::new(time_source, TokioSleep::new(), conn, fs);
628
629 let returned_token = harness
630 .expect_sso_token("newtoken", "2021-12-25T21:30:00Z")
631 .await;
632 let cached_token = load_cached_token(&harness.env, &harness.fs, "test")
633 .await
634 .unwrap();
635 assert_eq!(returned_token, cached_token);
636 assert_eq!(
637 "newrefreshtoken",
638 returned_token.refresh_token.unwrap().as_str()
639 );
640 assert_eq!(
641 "https://d-123.awsapps.com/start",
642 returned_token.start_url.unwrap()
643 );
644 assert_eq!("us-west-2", returned_token.region.unwrap().to_string());
645 assert_eq!("clientid", returned_token.client_id.unwrap());
646 assert_eq!(
647 "YSBzZWNyZXQ=",
648 returned_token.client_secret.unwrap().as_str()
649 );
650 assert_eq!(
651 SystemTime::UNIX_EPOCH + Duration::from_secs(1_671_975_000),
652 returned_token.registration_expires_at.unwrap()
653 );
654
655 let refresh_req = req_rx.expect_request();
656 let parsed_req: serde_json::Value =
657 serde_json::from_slice(refresh_req.body().bytes().unwrap()).unwrap();
658 let parsed_req = parsed_req.as_object().unwrap();
659 assert_eq!(
660 "clientid",
661 parsed_req.get("clientId").unwrap().as_str().unwrap()
662 );
663 assert_eq!(
664 "YSBzZWNyZXQ=",
665 parsed_req.get("clientSecret").unwrap().as_str().unwrap()
666 );
667 assert_eq!(
668 "refresh_token",
669 parsed_req.get("grantType").unwrap().as_str().unwrap()
670 );
671 assert_eq!(
672 "cachedrefreshtoken",
673 parsed_req.get("refreshToken").unwrap().as_str().unwrap()
674 );
675 }
676
677 #[cfg_attr(windows, ignore)]
679 #[tokio::test]
680 async fn expired_token_refresh_fails() {
681 let fs = Fs::from_slice(&[(
682 "/home/user/.aws/sso/cache/a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.json",
683 r#"
684 { "startUrl": "https://d-123.awsapps.com/start",
685 "region": "us-west-2",
686 "accessToken": "cachedtoken",
687 "expiresAt": "2021-12-25T13:00:00Z",
688 "clientId": "clientid",
689 "clientSecret": "YSBzZWNyZXQ=",
690 "registrationExpiresAt": "2022-12-25T13:30:00Z",
691 "refreshToken": "cachedrefreshtoken" }
692 "#,
693 )]);
694
695 let now = time("2021-12-25T13:30:00Z");
696 let time_source = SharedTimeSource::new(StaticTimeSource::new(now));
697
698 let (conn, req_rx) = capture_request(Some(
699 http::Response::builder()
700 .status(500)
701 .body(SdkBody::from(""))
702 .unwrap(),
703 ));
704 let harness = TestHarness::new(time_source, TokioSleep::new(), conn, fs);
705
706 let returned_token = harness
708 .expect_sso_token("cachedtoken", "2021-12-25T13:00:00Z")
709 .await;
710 let cached_token = load_cached_token(&harness.env, &harness.fs, "test")
711 .await
712 .unwrap();
713 assert_eq!(returned_token, cached_token);
714
715 let _ = req_rx.expect_request();
716 }
717
718 #[cfg_attr(windows, ignore)]
720 #[tokio::test]
722 async fn expired_token_refresh_without_new_refresh_token() {
723 let fs = Fs::from_slice(&[(
724 "/home/user/.aws/sso/cache/a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.json",
725 r#"
726 { "startUrl": "https://d-123.awsapps.com/start",
727 "region": "us-west-2",
728 "accessToken": "cachedtoken",
729 "expiresAt": "2021-12-25T13:00:00Z",
730 "clientId": "clientid",
731 "clientSecret": "YSBzZWNyZXQ=",
732 "registrationExpiresAt": "2022-12-25T13:30:00Z",
733 "refreshToken": "cachedrefreshtoken" }
734 "#,
735 )]);
736
737 let now = time("2021-12-25T13:30:00Z");
738 let time_source = SharedTimeSource::new(StaticTimeSource::new(now));
739
740 let (conn, req_rx) = capture_request(Some(
741 http::Response::builder()
742 .status(200)
743 .body(SdkBody::from(
744 r#"
745 { "tokenType": "Bearer",
746 "accessToken": "newtoken",
747 "expiresIn": 28800 }
748 "#,
749 ))
750 .unwrap(),
751 ));
752 let harness = TestHarness::new(time_source, TokioSleep::new(), conn, fs);
753
754 let returned_token = harness
755 .expect_sso_token("newtoken", "2021-12-25T21:30:00Z")
756 .await;
757 let cached_token = load_cached_token(&harness.env, &harness.fs, "test")
758 .await
759 .unwrap();
760 assert_eq!(returned_token, cached_token);
761 assert_eq!(
762 "cachedrefreshtoken",
763 returned_token.refresh_token.unwrap().as_str(),
764 "it should have kept the old refresh token"
765 );
766
767 let _ = req_rx.expect_request();
768 }
769
770 #[cfg_attr(windows, ignore)]
772 #[tokio::test]
773 async fn refresh_timings() {
774 let _logs = capture_test_logs();
775
776 let start_time = DateTime::from_str("2023-01-01T00:00:00Z", Format::DateTime).unwrap();
777 let (time_source, sleep_impl) = instant_time_and_sleep(start_time.try_into().unwrap());
778 let shared_time_source = SharedTimeSource::new(time_source.clone());
779
780 let fs = Fs::from_slice(&[(
781 "/home/user/.aws/sso/cache/a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.json",
782 r#"
783 { "startUrl": "https://d-123.awsapps.com/start",
784 "region": "us-west-2",
785 "accessToken": "first_token",
786 "_comment_expiresAt": "-------- Ten minutes after the start time: ------",
787 "expiresAt": "2023-01-01T00:10:00Z",
788 "clientId": "clientid",
789 "clientSecret": "YSBzZWNyZXQ=",
790 "registrationExpiresAt": "2023-01-02T12:00:00Z",
791 "refreshToken": "cachedrefreshtoken" }
792 "#,
793 )]);
794
795 let events = vec![
796 ReplayEvent::new(
798 http::Request::new(SdkBody::from("")), http::Response::builder()
800 .status(500)
801 .body(SdkBody::from(""))
802 .unwrap(),
803 ),
804 ReplayEvent::new(
806 http::Request::new(SdkBody::from("")), http::Response::builder()
808 .status(500)
809 .body(SdkBody::from(""))
810 .unwrap(),
811 ),
812 ReplayEvent::new(
814 http::Request::new(SdkBody::from("")), http::Response::builder()
816 .status(200)
817 .body(SdkBody::from(
818 r#"
819 { "tokenType": "Bearer",
820 "accessToken": "second_token",
821 "expiresIn": 28800 }
822 "#,
823 ))
824 .unwrap(),
825 ),
826 ];
827 let http_client = StaticReplayClient::new(events);
828 let harness = TestHarness::new(shared_time_source, sleep_impl, http_client, fs);
829
830 tracing::info!("test: first token retrieval should return the cached token");
831 assert!(
832 harness.last_refresh_attempt_time().is_none(),
833 "the last attempt time should start empty"
834 );
835 harness
836 .expect_token("first_token", "2023-01-01T00:10:00Z")
837 .await;
838 assert!(
839 harness.last_refresh_attempt_time().is_none(),
840 "it shouldn't have tried to refresh, so the last refresh attempt time shouldn't be set"
841 );
842
843 tracing::info!("test: advance 3 minutes");
844 time_source.advance(Duration::from_secs(3 * 60));
845
846 tracing::info!("test: the token shouldn't get refreshed since it's not in the 5 minute buffer time yet");
847 harness
848 .expect_token("first_token", "2023-01-01T00:10:00Z")
849 .await;
850 assert!(
851 harness.last_refresh_attempt_time().is_none(),
852 "it shouldn't have tried to refresh since the token isn't expiring soon"
853 );
854
855 tracing::info!("test: advance 2 minutes");
856 time_source.advance(Duration::from_secs(2 * 60));
857
858 tracing::info!(
859 "test: the token will fail to refresh, and the old cached token will be returned"
860 );
861 harness
862 .expect_token("first_token", "2023-01-01T00:10:00Z")
863 .await;
864 assert_eq!(
865 Some("2023-01-01T00:05:00Z"),
866 harness.last_refresh_attempt_time().as_deref(),
867 "it should update the last refresh attempt time since the expiration time is soon"
868 );
869
870 tracing::info!("test: advance 15 seconds");
871 time_source.advance(Duration::from_secs(15));
872
873 tracing::info!(
874 "test: the token will not refresh because the minimum time hasn't passed between attempts"
875 );
876 harness
877 .expect_token("first_token", "2023-01-01T00:10:00Z")
878 .await;
879
880 tracing::info!("test: advance 15 seconds");
881 time_source.advance(Duration::from_secs(15));
882
883 tracing::info!(
884 "test: the token will fail to refresh, and the old cached token will be returned"
885 );
886 harness
887 .expect_token("first_token", "2023-01-01T00:10:00Z")
888 .await;
889
890 tracing::info!("test: advance 30 seconds");
891 time_source.advance(Duration::from_secs(30));
892
893 tracing::info!("test: the token will refresh successfully");
894 harness
895 .expect_token("second_token", "2023-01-01T08:06:00Z")
896 .await;
897 }
898}