1use crate::build_errors::Error as BuilderError;
95use crate::constants::DEFAULT_SCOPE;
96use crate::credentials::dynamic::CredentialsProvider;
97use crate::credentials::{
98 CacheableResource, Credentials, build_credentials, extract_credential_type,
99};
100use crate::errors::{self, CredentialsError};
101use crate::headers_util::{
102 self, ACCESS_TOKEN_REQUEST_TYPE, build_cacheable_headers, metrics_header_value,
103};
104use crate::retry::{Builder as RetryTokenProviderBuilder, TokenProviderWithRetry};
105use crate::token::{CachedTokenProvider, Token, TokenProvider};
106use crate::token_cache::TokenCache;
107use crate::{BuildResult, Result};
108use async_trait::async_trait;
109use gax::backoff_policy::BackoffPolicyArg;
110use gax::retry_policy::RetryPolicyArg;
111use gax::retry_throttler::RetryThrottlerArg;
112use http::{Extensions, HeaderMap};
113use reqwest::Client;
114use serde_json::Value;
115use std::fmt::Debug;
116use std::sync::Arc;
117use std::time::Duration;
118use time::OffsetDateTime;
119use tokio::time::Instant;
120
121const IMPERSONATED_CREDENTIAL_TYPE: &str = "imp";
122pub(crate) const DEFAULT_LIFETIME: Duration = Duration::from_secs(3600);
123const MSG: &str = "failed to fetch token";
124
125enum BuilderSource {
126 FromJson(Value),
127 FromCredentials(Credentials),
128}
129
130pub struct Builder {
150 source: BuilderSource,
151 service_account_impersonation_url: Option<String>,
152 delegates: Option<Vec<String>>,
153 scopes: Option<Vec<String>>,
154 quota_project_id: Option<String>,
155 lifetime: Option<Duration>,
156 retry_builder: RetryTokenProviderBuilder,
157}
158
159impl Builder {
160 pub fn new(impersonated_credential: Value) -> Self {
168 Self {
169 source: BuilderSource::FromJson(impersonated_credential),
170 service_account_impersonation_url: None,
171 delegates: None,
172 scopes: None,
173 quota_project_id: None,
174 lifetime: None,
175 retry_builder: RetryTokenProviderBuilder::default(),
176 }
177 }
178
179 pub fn from_source_credentials(source_credentials: Credentials) -> Self {
197 Self {
198 source: BuilderSource::FromCredentials(source_credentials),
199 service_account_impersonation_url: None,
200 delegates: None,
201 scopes: None,
202 quota_project_id: None,
203 lifetime: None,
204 retry_builder: RetryTokenProviderBuilder::default(),
205 }
206 }
207
208 pub fn with_target_principal<S: Into<String>>(mut self, target_principal: S) -> Self {
225 self.service_account_impersonation_url = Some(format!(
226 "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:generateAccessToken",
227 target_principal.into()
228 ));
229 self
230 }
231
232 pub fn with_delegates<I, S>(mut self, delegates: I) -> Self
248 where
249 I: IntoIterator<Item = S>,
250 S: Into<String>,
251 {
252 self.delegates = Some(delegates.into_iter().map(|s| s.into()).collect());
253 self
254 }
255
256 pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
274 where
275 I: IntoIterator<Item = S>,
276 S: Into<String>,
277 {
278 self.scopes = Some(scopes.into_iter().map(|s| s.into()).collect());
279 self
280 }
281
282 pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
308 self.quota_project_id = Some(quota_project_id.into());
309 self
310 }
311
312 pub fn with_lifetime(mut self, lifetime: Duration) -> Self {
329 self.lifetime = Some(lifetime);
330 self
331 }
332
333 pub fn with_retry_policy<V: Into<RetryPolicyArg>>(mut self, v: V) -> Self {
350 self.retry_builder = self.retry_builder.with_retry_policy(v.into());
351 self
352 }
353
354 pub fn with_backoff_policy<V: Into<BackoffPolicyArg>>(mut self, v: V) -> Self {
372 self.retry_builder = self.retry_builder.with_backoff_policy(v.into());
373 self
374 }
375
376 pub fn with_retry_throttler<V: Into<RetryThrottlerArg>>(mut self, v: V) -> Self {
399 self.retry_builder = self.retry_builder.with_retry_throttler(v.into());
400 self
401 }
402
403 pub fn build(self) -> BuildResult<Credentials> {
420 let (token_provider, quota_project_id) = self.build_components()?;
421 Ok(Credentials {
422 inner: Arc::new(ImpersonatedServiceAccount {
423 token_provider: TokenCache::new(token_provider),
424 quota_project_id,
425 }),
426 })
427 }
428
429 fn build_components(
430 self,
431 ) -> BuildResult<(
432 TokenProviderWithRetry<ImpersonatedTokenProvider>,
433 Option<String>,
434 )> {
435 let (source_credentials, service_account_impersonation_url, delegates, quota_project_id) =
436 match self.source {
437 BuilderSource::FromJson(json) => {
438 let config = serde_json::from_value::<ImpersonatedConfig>(json)
439 .map_err(BuilderError::parsing)?;
440
441 let source_credential_type =
442 extract_credential_type(&config.source_credentials)?;
443 if source_credential_type == "impersonated_service_account" {
444 return Err(BuilderError::parsing(
445 "source credential of type `impersonated_service_account` is not supported. \
446 Use the `delegates` field to specify a delegation chain.",
447 ));
448 }
449
450 let source_credentials =
456 build_credentials(Some(config.source_credentials), None, None)?;
457
458 (
459 source_credentials,
460 config.service_account_impersonation_url,
461 config.delegates,
462 config.quota_project_id,
463 )
464 }
465 BuilderSource::FromCredentials(source_credentials) => {
466 let url = self.service_account_impersonation_url.ok_or_else(|| {
467 BuilderError::parsing("`service_account_impersonation_url` is required when building from source credentials")
468 })?;
469 (source_credentials, url, None, None)
470 }
471 };
472
473 let scopes = self
474 .scopes
475 .unwrap_or_else(|| vec![DEFAULT_SCOPE.to_string()]);
476
477 let quota_project_id = self.quota_project_id.or(quota_project_id);
478 let delegates = self.delegates.or(delegates);
479
480 let token_provider = ImpersonatedTokenProvider {
481 source_credentials,
482 service_account_impersonation_url,
483 delegates,
484 scopes,
485 lifetime: self.lifetime.unwrap_or(DEFAULT_LIFETIME),
486 };
487 let token_provider = self.retry_builder.build(token_provider);
488 Ok((token_provider, quota_project_id))
489 }
490}
491
492#[derive(serde::Deserialize, Debug, PartialEq)]
493struct ImpersonatedConfig {
494 service_account_impersonation_url: String,
495 source_credentials: Value,
496 delegates: Option<Vec<String>>,
497 quota_project_id: Option<String>,
498}
499
500#[derive(Debug)]
501struct ImpersonatedServiceAccount<T>
502where
503 T: CachedTokenProvider,
504{
505 token_provider: T,
506 quota_project_id: Option<String>,
507}
508
509#[async_trait::async_trait]
510impl<T> CredentialsProvider for ImpersonatedServiceAccount<T>
511where
512 T: CachedTokenProvider,
513{
514 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
515 let token = self.token_provider.token(extensions).await?;
516 build_cacheable_headers(&token, &self.quota_project_id)
517 }
518}
519
520struct ImpersonatedTokenProvider {
521 source_credentials: Credentials,
522 service_account_impersonation_url: String,
523 delegates: Option<Vec<String>>,
524 scopes: Vec<String>,
525 lifetime: Duration,
526}
527
528impl Debug for ImpersonatedTokenProvider {
529 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
530 f.debug_struct("ImpersonatedTokenProvider")
531 .field("source_credentials", &self.source_credentials)
532 .field(
533 "service_account_impersonation_url",
534 &self.service_account_impersonation_url,
535 )
536 .field("delegates", &self.delegates)
537 .field("scopes", &self.scopes)
538 .field("lifetime", &self.lifetime)
539 .finish()
540 }
541}
542
543#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]
544struct GenerateAccessTokenRequest {
545 #[serde(skip_serializing_if = "Option::is_none")]
546 delegates: Option<Vec<String>>,
547 scope: Vec<String>,
548 lifetime: String,
549}
550
551pub(crate) async fn generate_access_token(
552 source_headers: HeaderMap,
553 delegates: Option<Vec<String>>,
554 scopes: Vec<String>,
555 lifetime: Duration,
556 service_account_impersonation_url: &str,
557) -> Result<Token> {
558 let client = Client::new();
559 let body = GenerateAccessTokenRequest {
560 delegates,
561 scope: scopes,
562 lifetime: format!("{}s", lifetime.as_secs_f64()),
563 };
564
565 let response = client
566 .post(service_account_impersonation_url)
567 .header("Content-Type", "application/json")
568 .header(
569 headers_util::X_GOOG_API_CLIENT,
570 metrics_header_value(ACCESS_TOKEN_REQUEST_TYPE, IMPERSONATED_CREDENTIAL_TYPE),
571 )
572 .headers(source_headers)
573 .json(&body)
574 .send()
575 .await
576 .map_err(|e| errors::from_http_error(e, MSG))?;
577
578 if !response.status().is_success() {
579 let err = errors::from_http_response(response, MSG).await;
580 return Err(err);
581 }
582
583 let token_response = response
584 .json::<GenerateAccessTokenResponse>()
585 .await
586 .map_err(|e| {
587 let retryable = !e.is_decode();
588 CredentialsError::from_source(retryable, e)
589 })?;
590
591 let parsed_dt = OffsetDateTime::parse(
592 &token_response.expire_time,
593 &time::format_description::well_known::Rfc3339,
594 )
595 .map_err(errors::non_retryable)?;
596
597 let remaining_duration = parsed_dt - OffsetDateTime::now_utc();
598 let expires_at = Instant::now() + remaining_duration.try_into().unwrap();
599
600 let token = Token {
601 token: token_response.access_token,
602 token_type: "Bearer".to_string(),
603 expires_at: Some(expires_at),
604 metadata: None,
605 };
606 Ok(token)
607}
608
609#[async_trait]
610impl TokenProvider for ImpersonatedTokenProvider {
611 async fn token(&self) -> Result<Token> {
612 let source_headers = self.source_credentials.headers(Extensions::new()).await?;
613 let source_headers = match source_headers {
614 CacheableResource::New { data, .. } => data,
615 CacheableResource::NotModified => {
616 unreachable!("requested source credentials without a caching etag")
617 }
618 };
619 generate_access_token(
620 source_headers,
621 self.delegates.clone(),
622 self.scopes.clone(),
623 self.lifetime,
624 &self.service_account_impersonation_url,
625 )
626 .await
627 }
628}
629
630#[derive(serde::Deserialize)]
631struct GenerateAccessTokenResponse {
632 #[serde(rename = "accessToken")]
633 access_token: String,
634 #[serde(rename = "expireTime")]
635 expire_time: String,
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641 use crate::credentials::tests::{
642 get_mock_auth_retry_policy, get_mock_backoff_policy, get_mock_retry_throttler,
643 };
644 use httptest::cycle;
645 use httptest::{Expectation, Server, matchers::*, responders::*};
646 use serde_json::json;
647
648 type TestResult = anyhow::Result<()>;
649
650 #[tokio::test]
651 async fn test_generate_access_token_success() -> TestResult {
652 let server = Server::run();
653 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
654 .format(&time::format_description::well_known::Rfc3339)
655 .unwrap();
656 server.expect(
657 Expectation::matching(all_of![
658 request::method_path(
659 "POST",
660 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
661 ),
662 request::headers(contains(("authorization", "Bearer test-token"))),
663 ])
664 .respond_with(json_encoded(json!({
665 "accessToken": "test-impersonated-token",
666 "expireTime": expire_time
667 }))),
668 );
669
670 let mut headers = HeaderMap::new();
671 headers.insert("authorization", "Bearer test-token".parse().unwrap());
672 let token = generate_access_token(
673 headers,
674 None,
675 vec!["scope".to_string()],
676 DEFAULT_LIFETIME,
677 &server
678 .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
679 .to_string(),
680 )
681 .await?;
682
683 assert_eq!(token.token, "test-impersonated-token");
684 Ok(())
685 }
686
687 #[tokio::test]
688 async fn test_generate_access_token_403() -> TestResult {
689 let server = Server::run();
690 server.expect(
691 Expectation::matching(all_of![
692 request::method_path(
693 "POST",
694 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
695 ),
696 request::headers(contains(("authorization", "Bearer test-token"))),
697 ])
698 .respond_with(status_code(403)),
699 );
700
701 let mut headers = HeaderMap::new();
702 headers.insert("authorization", "Bearer test-token".parse().unwrap());
703 let err = generate_access_token(
704 headers,
705 None,
706 vec!["scope".to_string()],
707 DEFAULT_LIFETIME,
708 &server
709 .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
710 .to_string(),
711 )
712 .await
713 .unwrap_err();
714
715 assert!(!err.is_transient());
716 Ok(())
717 }
718
719 #[tokio::test]
720 async fn test_generate_access_token_no_auth_header() -> TestResult {
721 let server = Server::run();
722 server.expect(
723 Expectation::matching(request::method_path(
724 "POST",
725 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
726 ))
727 .respond_with(status_code(401)),
728 );
729
730 let err = generate_access_token(
731 HeaderMap::new(),
732 None,
733 vec!["scope".to_string()],
734 DEFAULT_LIFETIME,
735 &server
736 .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
737 .to_string(),
738 )
739 .await
740 .unwrap_err();
741
742 assert!(!err.is_transient());
743 Ok(())
744 }
745
746 #[tokio::test]
747 async fn test_impersonated_service_account() -> TestResult {
748 let server = Server::run();
749 server.expect(
750 Expectation::matching(request::method_path("POST", "/token")).respond_with(
751 json_encoded(json!({
752 "access_token": "test-user-account-token",
753 "expires_in": 3600,
754 "token_type": "Bearer",
755 })),
756 ),
757 );
758 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
759 .format(&time::format_description::well_known::Rfc3339)
760 .unwrap();
761 server.expect(
762 Expectation::matching(all_of![
763 request::method_path(
764 "POST",
765 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
766 ),
767 request::headers(contains((
768 "authorization",
769 "Bearer test-user-account-token"
770 ))),
771 request::body(json_decoded(eq(json!({
772 "scope": ["scope1", "scope2"],
773 "lifetime": "3600s"
774 }))))
775 ])
776 .respond_with(json_encoded(json!({
777 "accessToken": "test-impersonated-token",
778 "expireTime": expire_time
779 }))),
780 );
781
782 let impersonated_credential = json!({
783 "type": "impersonated_service_account",
784 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
785 "source_credentials": {
786 "type": "authorized_user",
787 "client_id": "test-client-id",
788 "client_secret": "test-client-secret",
789 "refresh_token": "test-refresh-token",
790 "token_uri": server.url("/token").to_string()
791 }
792 });
793 let (token_provider, _) = Builder::new(impersonated_credential)
794 .with_scopes(vec!["scope1", "scope2"])
795 .build_components()?;
796
797 let token = token_provider.token().await?;
798 assert_eq!(token.token, "test-impersonated-token");
799 assert_eq!(token.token_type, "Bearer");
800
801 Ok(())
802 }
803
804 #[tokio::test]
805 async fn test_impersonated_service_account_default_scope() -> TestResult {
806 let server = Server::run();
807 server.expect(
808 Expectation::matching(request::method_path("POST", "/token")).respond_with(
809 json_encoded(json!({
810 "access_token": "test-user-account-token",
811 "expires_in": 3600,
812 "token_type": "Bearer",
813 })),
814 ),
815 );
816 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
817 .format(&time::format_description::well_known::Rfc3339)
818 .unwrap();
819 server.expect(
820 Expectation::matching(all_of![
821 request::method_path(
822 "POST",
823 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
824 ),
825 request::headers(contains((
826 "authorization",
827 "Bearer test-user-account-token"
828 ))),
829 request::body(json_decoded(eq(json!({
830 "scope": [DEFAULT_SCOPE],
831 "lifetime": "3600s"
832 }))))
833 ])
834 .respond_with(json_encoded(json!({
835 "accessToken": "test-impersonated-token",
836 "expireTime": expire_time
837 }))),
838 );
839
840 let impersonated_credential = json!({
841 "type": "impersonated_service_account",
842 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
843 "source_credentials": {
844 "type": "authorized_user",
845 "client_id": "test-client-id",
846 "client_secret": "test-client-secret",
847 "refresh_token": "test-refresh-token",
848 "token_uri": server.url("/token").to_string()
849 }
850 });
851 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
852
853 let token = token_provider.token().await?;
854 assert_eq!(token.token, "test-impersonated-token");
855 assert_eq!(token.token_type, "Bearer");
856
857 Ok(())
858 }
859
860 #[tokio::test]
861 async fn test_impersonated_service_account_with_custom_lifetime() -> TestResult {
862 let server = Server::run();
863 server.expect(
864 Expectation::matching(request::method_path("POST", "/token")).respond_with(
865 json_encoded(json!({
866 "access_token": "test-user-account-token",
867 "expires_in": 3600,
868 "token_type": "Bearer",
869 })),
870 ),
871 );
872 let expire_time = (OffsetDateTime::now_utc() + time::Duration::seconds(500))
873 .format(&time::format_description::well_known::Rfc3339)
874 .unwrap();
875 server.expect(
876 Expectation::matching(all_of![
877 request::method_path(
878 "POST",
879 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
880 ),
881 request::headers(contains((
882 "authorization",
883 "Bearer test-user-account-token"
884 ))),
885 request::body(json_decoded(eq(json!({
886 "scope": ["scope1", "scope2"],
887 "lifetime": "3.5s"
888 }))))
889 ])
890 .respond_with(json_encoded(json!({
891 "accessToken": "test-impersonated-token",
892 "expireTime": expire_time
893 }))),
894 );
895
896 let impersonated_credential = json!({
897 "type": "impersonated_service_account",
898 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
899 "source_credentials": {
900 "type": "authorized_user",
901 "client_id": "test-client-id",
902 "client_secret": "test-client-secret",
903 "refresh_token": "test-refresh-token",
904 "token_uri": server.url("/token").to_string()
905 }
906 });
907 let (token_provider, _) = Builder::new(impersonated_credential)
908 .with_scopes(vec!["scope1", "scope2"])
909 .with_lifetime(Duration::from_secs_f32(3.5))
910 .build_components()?;
911
912 let token = token_provider.token().await?;
913 assert_eq!(token.token, "test-impersonated-token");
914
915 Ok(())
916 }
917
918 #[tokio::test]
919 async fn test_with_delegates() -> TestResult {
920 let server = Server::run();
921 server.expect(
922 Expectation::matching(request::method_path("POST", "/token")).respond_with(
923 json_encoded(json!({
924 "access_token": "test-user-account-token",
925 "expires_in": 3600,
926 "token_type": "Bearer",
927 })),
928 ),
929 );
930 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
931 .format(&time::format_description::well_known::Rfc3339)
932 .unwrap();
933 server.expect(
934 Expectation::matching(all_of![
935 request::method_path(
936 "POST",
937 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
938 ),
939 request::headers(contains((
940 "authorization",
941 "Bearer test-user-account-token"
942 ))),
943 request::body(json_decoded(eq(json!({
944 "scope": [DEFAULT_SCOPE],
945 "lifetime": "3600s",
946 "delegates": ["delegate1", "delegate2"]
947 }))))
948 ])
949 .respond_with(json_encoded(json!({
950 "accessToken": "test-impersonated-token",
951 "expireTime": expire_time
952 }))),
953 );
954
955 let impersonated_credential = json!({
956 "type": "impersonated_service_account",
957 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
958 "source_credentials": {
959 "type": "authorized_user",
960 "client_id": "test-client-id",
961 "client_secret": "test-client-secret",
962 "refresh_token": "test-refresh-token",
963 "token_uri": server.url("/token").to_string()
964 }
965 });
966 let (token_provider, _) = Builder::new(impersonated_credential)
967 .with_delegates(vec!["delegate1", "delegate2"])
968 .build_components()?;
969
970 let token = token_provider.token().await?;
971 assert_eq!(token.token, "test-impersonated-token");
972 assert_eq!(token.token_type, "Bearer");
973
974 Ok(())
975 }
976
977 #[tokio::test]
978 async fn test_impersonated_service_account_fail() -> TestResult {
979 let server = Server::run();
980 server.expect(
981 Expectation::matching(request::method_path("POST", "/token")).respond_with(
982 json_encoded(json!({
983 "access_token": "test-user-account-token",
984 "expires_in": 3600,
985 "token_type": "Bearer",
986 })),
987 ),
988 );
989 server.expect(
990 Expectation::matching(request::method_path(
991 "POST",
992 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
993 ))
994 .respond_with(status_code(500)),
995 );
996
997 let impersonated_credential = json!({
998 "type": "impersonated_service_account",
999 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1000 "source_credentials": {
1001 "type": "authorized_user",
1002 "client_id": "test-client-id",
1003 "client_secret": "test-client-secret",
1004 "refresh_token": "test-refresh-token",
1005 "token_uri": server.url("/token").to_string()
1006 }
1007 });
1008 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1009
1010 let err = token_provider.token().await.unwrap_err();
1011 assert!(err.is_transient());
1012
1013 Ok(())
1014 }
1015
1016 #[tokio::test]
1017 async fn debug_token_provider() {
1018 let source_credentials = crate::credentials::user_account::Builder::new(json!({
1019 "type": "authorized_user",
1020 "client_id": "test-client-id",
1021 "client_secret": "test-client-secret",
1022 "refresh_token": "test-refresh-token"
1023 }))
1024 .build()
1025 .unwrap();
1026
1027 let expected = ImpersonatedTokenProvider {
1028 source_credentials,
1029 service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1030 delegates: Some(vec!["delegate1".to_string()]),
1031 scopes: vec!["scope1".to_string()],
1032 lifetime: Duration::from_secs(3600),
1033 };
1034 let fmt = format!("{expected:?}");
1035 assert!(fmt.contains("UserCredentials"), "{fmt}");
1036 assert!(fmt.contains("test-client-id"), "{fmt}");
1037 assert!(!fmt.contains("test-client-secret"), "{fmt}");
1038 assert!(!fmt.contains("test-refresh-token"), "{fmt}");
1039 assert!(fmt.contains("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"), "{fmt}");
1040 assert!(fmt.contains("delegate1"), "{fmt}");
1041 assert!(fmt.contains("scope1"), "{fmt}");
1042 assert!(fmt.contains("3600s"), "{fmt}");
1043 }
1044
1045 #[test]
1046 fn impersonated_config_full_from_json_success() {
1047 let source_credentials_json = json!({
1048 "type": "authorized_user",
1049 "client_id": "test-client-id",
1050 "client_secret": "test-client-secret",
1051 "refresh_token": "test-refresh-token"
1052 });
1053 let json = json!({
1054 "type": "impersonated_service_account",
1055 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1056 "source_credentials": source_credentials_json,
1057 "delegates": ["delegate1"],
1058 "quota_project_id": "test-project-id",
1059 });
1060
1061 let expected = ImpersonatedConfig {
1062 service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1063 source_credentials: source_credentials_json,
1064 delegates: Some(vec!["delegate1".to_string()]),
1065 quota_project_id: Some("test-project-id".to_string()),
1066 };
1067 let actual: ImpersonatedConfig = serde_json::from_value(json).unwrap();
1068 assert_eq!(actual, expected);
1069 }
1070
1071 #[test]
1072 fn impersonated_config_partial_from_json_success() {
1073 let source_credentials_json = json!({
1074 "type": "authorized_user",
1075 "client_id": "test-client-id",
1076 "client_secret": "test-client-secret",
1077 "refresh_token": "test-refresh-token"
1078 });
1079 let json = json!({
1080 "type": "impersonated_service_account",
1081 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1082 "source_credentials": source_credentials_json
1083 });
1084
1085 let config: ImpersonatedConfig = serde_json::from_value(json).unwrap();
1086 assert_eq!(
1087 config.service_account_impersonation_url,
1088 "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1089 );
1090 assert_eq!(config.source_credentials, source_credentials_json);
1091 assert_eq!(config.delegates, None);
1092 assert_eq!(config.quota_project_id, None);
1093 }
1094
1095 #[tokio::test]
1096 async fn test_impersonated_service_account_source_fail() -> TestResult {
1097 #[derive(Debug)]
1098 struct MockSourceCredentialsFail;
1099
1100 #[async_trait]
1101 impl CredentialsProvider for MockSourceCredentialsFail {
1102 async fn headers(
1103 &self,
1104 _extensions: Extensions,
1105 ) -> Result<CacheableResource<HeaderMap>> {
1106 Err(errors::non_retryable_from_str("source failed"))
1107 }
1108 }
1109
1110 let source_credentials = Credentials {
1111 inner: Arc::new(MockSourceCredentialsFail),
1112 };
1113
1114 let token_provider = ImpersonatedTokenProvider {
1115 source_credentials,
1116 service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1117 delegates: Some(vec!["delegate1".to_string()]),
1118 scopes: vec!["scope1".to_string()],
1119 lifetime: DEFAULT_LIFETIME,
1120 };
1121
1122 let err = token_provider.token().await.unwrap_err();
1123 assert!(err.to_string().contains("source failed"));
1124
1125 Ok(())
1126 }
1127
1128 #[tokio::test]
1129 async fn test_missing_impersonation_url_fail() {
1130 let source_credentials = crate::credentials::user_account::Builder::new(json!({
1131 "type": "authorized_user",
1132 "client_id": "test-client-id",
1133 "client_secret": "test-client-secret",
1134 "refresh_token": "test-refresh-token"
1135 }))
1136 .build()
1137 .unwrap();
1138
1139 let result = Builder::from_source_credentials(source_credentials).build();
1140 assert!(result.is_err());
1141 let err = result.unwrap_err();
1142 assert!(err.is_parsing());
1143 assert!(
1144 err.to_string()
1145 .contains("`service_account_impersonation_url` is required")
1146 );
1147 }
1148
1149 #[tokio::test]
1150 async fn test_nested_impersonated_credentials_fail() {
1151 let nested_impersonated = json!({
1152 "type": "impersonated_service_account",
1153 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1154 "source_credentials": {
1155 "type": "impersonated_service_account",
1156 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1157 "source_credentials": {
1158 "type": "authorized_user",
1159 "client_id": "test-client-id",
1160 "client_secret": "test-client-secret",
1161 "refresh_token": "test-refresh-token"
1162 }
1163 }
1164 });
1165
1166 let result = Builder::new(nested_impersonated).build();
1167 assert!(result.is_err());
1168 let err = result.unwrap_err();
1169 assert!(err.is_parsing());
1170 assert!(
1171 err.to_string().contains(
1172 "source credential of type `impersonated_service_account` is not supported"
1173 )
1174 );
1175 }
1176
1177 #[tokio::test]
1178 async fn test_malformed_impersonated_credentials_fail() {
1179 let malformed_impersonated = json!({
1180 "type": "impersonated_service_account",
1181 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1182 });
1183
1184 let result = Builder::new(malformed_impersonated).build();
1185 assert!(result.is_err());
1186 let err = result.unwrap_err();
1187 assert!(err.is_parsing());
1188 assert!(
1189 err.to_string()
1190 .contains("missing field `source_credentials`")
1191 );
1192 }
1193
1194 #[tokio::test]
1195 async fn test_invalid_source_credential_type_fail() {
1196 let invalid_source = json!({
1197 "type": "impersonated_service_account",
1198 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1199 "source_credentials": {
1200 "type": "invalid_type",
1201 }
1202 });
1203
1204 let result = Builder::new(invalid_source).build();
1205 assert!(result.is_err());
1206 let err = result.unwrap_err();
1207 assert!(err.is_unknown_type());
1208 }
1209
1210 #[tokio::test]
1211 async fn test_missing_expiry() -> TestResult {
1212 let server = Server::run();
1213 server.expect(
1214 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1215 json_encoded(json!({
1216 "access_token": "test-user-account-token",
1217 "expires_in": 3600,
1218 "token_type": "Bearer",
1219 })),
1220 ),
1221 );
1222 server.expect(
1223 Expectation::matching(request::method_path(
1224 "POST",
1225 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1226 ))
1227 .respond_with(json_encoded(json!({
1228 "accessToken": "test-impersonated-token",
1229 }))),
1230 );
1231
1232 let impersonated_credential = json!({
1233 "type": "impersonated_service_account",
1234 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1235 "source_credentials": {
1236 "type": "authorized_user",
1237 "client_id": "test-client-id",
1238 "client_secret": "test-client-secret",
1239 "refresh_token": "test-refresh-token",
1240 "token_uri": server.url("/token").to_string()
1241 }
1242 });
1243 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1244
1245 let err = token_provider.token().await.unwrap_err();
1246 assert!(!err.is_transient());
1247
1248 Ok(())
1249 }
1250
1251 #[tokio::test]
1252 async fn test_invalid_expiry_format() -> TestResult {
1253 let server = Server::run();
1254 server.expect(
1255 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1256 json_encoded(json!({
1257 "access_token": "test-user-account-token",
1258 "expires_in": 3600,
1259 "token_type": "Bearer",
1260 })),
1261 ),
1262 );
1263 server.expect(
1264 Expectation::matching(request::method_path(
1265 "POST",
1266 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1267 ))
1268 .respond_with(json_encoded(json!({
1269 "accessToken": "test-impersonated-token",
1270 "expireTime": "invalid-format"
1271 }))),
1272 );
1273
1274 let impersonated_credential = json!({
1275 "type": "impersonated_service_account",
1276 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1277 "source_credentials": {
1278 "type": "authorized_user",
1279 "client_id": "test-client-id",
1280 "client_secret": "test-client-secret",
1281 "refresh_token": "test-refresh-token",
1282 "token_uri": server.url("/token").to_string()
1283 }
1284 });
1285 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1286
1287 let err = token_provider.token().await.unwrap_err();
1288 assert!(!err.is_transient());
1289
1290 Ok(())
1291 }
1292
1293 #[tokio::test]
1294 async fn token_provider_malformed_response_is_nonretryable() -> TestResult {
1295 let server = Server::run();
1296 server.expect(
1297 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1298 json_encoded(json!({
1299 "access_token": "test-user-account-token",
1300 "expires_in": 3600,
1301 "token_type": "Bearer",
1302 })),
1303 ),
1304 );
1305 server.expect(
1306 Expectation::matching(request::method_path(
1307 "POST",
1308 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1309 ))
1310 .respond_with(json_encoded(json!("bad json"))),
1311 );
1312
1313 let impersonated_credential = json!({
1314 "type": "impersonated_service_account",
1315 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1316 "source_credentials": {
1317 "type": "authorized_user",
1318 "client_id": "test-client-id",
1319 "client_secret": "test-client-secret",
1320 "refresh_token": "test-refresh-token",
1321 "token_uri": server.url("/token").to_string()
1322 }
1323 });
1324 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1325
1326 let e = token_provider.token().await.err().unwrap();
1327 assert!(!e.is_transient(), "{e}");
1328
1329 Ok(())
1330 }
1331
1332 #[tokio::test]
1333 async fn token_provider_nonretryable_error() -> TestResult {
1334 let server = Server::run();
1335 server.expect(
1336 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1337 json_encoded(json!({
1338 "access_token": "test-user-account-token",
1339 "expires_in": 3600,
1340 "token_type": "Bearer",
1341 })),
1342 ),
1343 );
1344 server.expect(
1345 Expectation::matching(request::method_path(
1346 "POST",
1347 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1348 ))
1349 .respond_with(status_code(401)),
1350 );
1351
1352 let impersonated_credential = json!({
1353 "type": "impersonated_service_account",
1354 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1355 "source_credentials": {
1356 "type": "authorized_user",
1357 "client_id": "test-client-id",
1358 "client_secret": "test-client-secret",
1359 "refresh_token": "test-refresh-token",
1360 "token_uri": server.url("/token").to_string()
1361 }
1362 });
1363 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1364
1365 let err = token_provider.token().await.unwrap_err();
1366 assert!(!err.is_transient());
1367
1368 Ok(())
1369 }
1370
1371 #[tokio::test]
1372 async fn credential_full_with_quota_project_from_builder() -> TestResult {
1373 let server = Server::run();
1374 server.expect(
1375 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1376 json_encoded(json!({
1377 "access_token": "test-user-account-token",
1378 "expires_in": 3600,
1379 "token_type": "Bearer",
1380 })),
1381 ),
1382 );
1383 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1384 .format(&time::format_description::well_known::Rfc3339)
1385 .unwrap();
1386 server.expect(
1387 Expectation::matching(request::method_path(
1388 "POST",
1389 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1390 ))
1391 .respond_with(json_encoded(json!({
1392 "accessToken": "test-impersonated-token",
1393 "expireTime": expire_time
1394 }))),
1395 );
1396
1397 let impersonated_credential = json!({
1398 "type": "impersonated_service_account",
1399 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1400 "source_credentials": {
1401 "type": "authorized_user",
1402 "client_id": "test-client-id",
1403 "client_secret": "test-client-secret",
1404 "refresh_token": "test-refresh-token",
1405 "token_uri": server.url("/token").to_string()
1406 }
1407 });
1408 let creds = Builder::new(impersonated_credential)
1409 .with_quota_project_id("test-project")
1410 .build()?;
1411
1412 let headers = creds.headers(Extensions::new()).await?;
1413 match headers {
1414 CacheableResource::New { data, .. } => {
1415 assert_eq!(data.get("x-goog-user-project").unwrap(), "test-project");
1416 }
1417 CacheableResource::NotModified => panic!("Expected new headers, but got NotModified"),
1418 }
1419
1420 Ok(())
1421 }
1422
1423 #[tokio::test]
1424 async fn test_with_target_principal() {
1425 let source_credentials = crate::credentials::user_account::Builder::new(json!({
1426 "type": "authorized_user",
1427 "client_id": "test-client-id",
1428 "client_secret": "test-client-secret",
1429 "refresh_token": "test-refresh-token"
1430 }))
1431 .build()
1432 .unwrap();
1433
1434 let (token_provider, _) = Builder::from_source_credentials(source_credentials)
1435 .with_target_principal("test-principal@example.iam.gserviceaccount.com")
1436 .build_components()
1437 .unwrap();
1438
1439 assert_eq!(
1440 token_provider.inner.service_account_impersonation_url,
1441 "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal@example.iam.gserviceaccount.com:generateAccessToken"
1442 );
1443 }
1444
1445 #[tokio::test]
1446 async fn credential_full_with_quota_project_from_json() -> TestResult {
1447 let server = Server::run();
1448 server.expect(
1449 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1450 json_encoded(json!({
1451 "access_token": "test-user-account-token",
1452 "expires_in": 3600,
1453 "token_type": "Bearer",
1454 })),
1455 ),
1456 );
1457 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1458 .format(&time::format_description::well_known::Rfc3339)
1459 .unwrap();
1460 server.expect(
1461 Expectation::matching(request::method_path(
1462 "POST",
1463 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1464 ))
1465 .respond_with(json_encoded(json!({
1466 "accessToken": "test-impersonated-token",
1467 "expireTime": expire_time
1468 }))),
1469 );
1470
1471 let impersonated_credential = json!({
1472 "type": "impersonated_service_account",
1473 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1474 "source_credentials": {
1475 "type": "authorized_user",
1476 "client_id": "test-client-id",
1477 "client_secret": "test-client-secret",
1478 "refresh_token": "test-refresh-token",
1479 "token_uri": server.url("/token").to_string()
1480 },
1481 "quota_project_id": "test-project-from-json",
1482 });
1483
1484 let creds = Builder::new(impersonated_credential).build()?;
1485
1486 let headers = creds.headers(Extensions::new()).await?;
1487 match headers {
1488 CacheableResource::New { data, .. } => {
1489 assert_eq!(
1490 data.get("x-goog-user-project").unwrap(),
1491 "test-project-from-json"
1492 );
1493 }
1494 CacheableResource::NotModified => panic!("Expected new headers, but got NotModified"),
1495 }
1496
1497 Ok(())
1498 }
1499
1500 #[tokio::test]
1501 async fn test_impersonated_does_not_propagate_settings_to_source() -> TestResult {
1502 let server = Server::run();
1503
1504 server.expect(
1507 Expectation::matching(all_of![
1508 request::method_path("POST", "/source_token"),
1509 request::body(json_decoded(
1510 |body: &serde_json::Value| body["scopes"].is_null()
1511 ))
1512 ])
1513 .respond_with(json_encoded(json!({
1514 "access_token": "source-token",
1515 "expires_in": 3600,
1516 "token_type": "Bearer",
1517 }))),
1518 );
1519
1520 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1521 .format(&time::format_description::well_known::Rfc3339)
1522 .unwrap();
1523
1524 server.expect(
1527 Expectation::matching(all_of![
1528 request::method_path(
1529 "POST",
1530 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1531 ),
1532 request::headers(contains(("authorization", "Bearer source-token"))),
1533 request::body(json_decoded(eq(json!({
1534 "scope": ["impersonated-scope"],
1535 "lifetime": "3600s"
1536 }))))
1537 ])
1538 .respond_with(json_encoded(json!({
1539 "accessToken": "impersonated-token",
1540 "expireTime": expire_time
1541 }))),
1542 );
1543
1544 let impersonated_credential = json!({
1545 "type": "impersonated_service_account",
1546 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1547 "source_credentials": {
1548 "type": "authorized_user",
1549 "client_id": "test-client-id",
1550 "client_secret": "test-client-secret",
1551 "refresh_token": "test-refresh-token",
1552 "token_uri": server.url("/source_token").to_string()
1553 }
1554 });
1555
1556 let creds = Builder::new(impersonated_credential)
1557 .with_scopes(vec!["impersonated-scope"])
1558 .with_quota_project_id("impersonated-quota-project")
1559 .build()?;
1560
1561 let fmt = format!("{creds:?}");
1563 assert!(fmt.contains("impersonated-quota-project"));
1564
1565 let _token = creds.headers(Extensions::new()).await?;
1567
1568 Ok(())
1569 }
1570
1571 #[tokio::test]
1572 async fn test_impersonated_metrics_header() -> TestResult {
1573 let server = Server::run();
1574 server.expect(
1575 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1576 json_encoded(json!({
1577 "access_token": "test-user-account-token",
1578 "expires_in": 3600,
1579 "token_type": "Bearer",
1580 })),
1581 ),
1582 );
1583 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1584 .format(&time::format_description::well_known::Rfc3339)
1585 .unwrap();
1586 server.expect(
1587 Expectation::matching(all_of![
1588 request::method_path(
1589 "POST",
1590 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1591 ),
1592 request::headers(contains(("x-goog-api-client", matches("cred-type/imp")))),
1593 request::headers(contains((
1594 "x-goog-api-client",
1595 matches("auth-request-type/at")
1596 )))
1597 ])
1598 .respond_with(json_encoded(json!({
1599 "accessToken": "test-impersonated-token",
1600 "expireTime": expire_time
1601 }))),
1602 );
1603
1604 let impersonated_credential = json!({
1605 "type": "impersonated_service_account",
1606 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1607 "source_credentials": {
1608 "type": "authorized_user",
1609 "client_id": "test-client-id",
1610 "client_secret": "test-client-secret",
1611 "refresh_token": "test-refresh-token",
1612 "token_uri": server.url("/token").to_string()
1613 }
1614 });
1615 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1616
1617 let token = token_provider.token().await?;
1618 assert_eq!(token.token, "test-impersonated-token");
1619 assert_eq!(token.token_type, "Bearer");
1620
1621 Ok(())
1622 }
1623
1624 #[tokio::test]
1625 async fn test_impersonated_retries_for_success() -> TestResult {
1626 let mut server = Server::run();
1627 server.expect(
1629 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1630 json_encoded(json!({
1631 "access_token": "test-user-account-token",
1632 "expires_in": 3600,
1633 "token_type": "Bearer",
1634 })),
1635 ),
1636 );
1637
1638 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1639 .format(&time::format_description::well_known::Rfc3339)
1640 .unwrap();
1641
1642 let impersonation_path =
1644 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
1645 server.expect(
1646 Expectation::matching(request::method_path("POST", impersonation_path))
1647 .times(3)
1648 .respond_with(cycle![
1649 status_code(503).body("try-again"),
1650 status_code(503).body("try-again"),
1651 status_code(200)
1652 .append_header("Content-Type", "application/json")
1653 .body(
1654 json!({
1655 "accessToken": "test-impersonated-token",
1656 "expireTime": expire_time
1657 })
1658 .to_string()
1659 ),
1660 ]),
1661 );
1662
1663 let impersonated_credential = json!({
1664 "type": "impersonated_service_account",
1665 "service_account_impersonation_url": server.url(impersonation_path).to_string(),
1666 "source_credentials": {
1667 "type": "authorized_user",
1668 "client_id": "test-client-id",
1669 "client_secret": "test-client-secret",
1670 "refresh_token": "test-refresh-token",
1671 "token_uri": server.url("/token").to_string()
1672 }
1673 });
1674
1675 let (token_provider, _) = Builder::new(impersonated_credential)
1676 .with_retry_policy(get_mock_auth_retry_policy(3))
1677 .with_backoff_policy(get_mock_backoff_policy())
1678 .with_retry_throttler(get_mock_retry_throttler())
1679 .build_components()?;
1680
1681 let token = token_provider.token().await?;
1682 assert_eq!(token.token, "test-impersonated-token");
1683
1684 server.verify_and_clear();
1685 Ok(())
1686 }
1687
1688 #[tokio::test]
1689 async fn test_impersonated_does_not_retry_on_non_transient_failures() -> TestResult {
1690 let mut server = Server::run();
1691 server.expect(
1693 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1694 json_encoded(json!({
1695 "access_token": "test-user-account-token",
1696 "expires_in": 3600,
1697 "token_type": "Bearer",
1698 })),
1699 ),
1700 );
1701
1702 let impersonation_path =
1704 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
1705 server.expect(
1706 Expectation::matching(request::method_path("POST", impersonation_path))
1707 .times(1)
1708 .respond_with(status_code(401)),
1709 );
1710
1711 let impersonated_credential = json!({
1712 "type": "impersonated_service_account",
1713 "service_account_impersonation_url": server.url(impersonation_path).to_string(),
1714 "source_credentials": {
1715 "type": "authorized_user",
1716 "client_id": "test-client-id",
1717 "client_secret": "test-client-secret",
1718 "refresh_token": "test-refresh-token",
1719 "token_uri": server.url("/token").to_string()
1720 }
1721 });
1722
1723 let (token_provider, _) = Builder::new(impersonated_credential)
1724 .with_retry_policy(get_mock_auth_retry_policy(3))
1725 .with_backoff_policy(get_mock_backoff_policy())
1726 .with_retry_throttler(get_mock_retry_throttler())
1727 .build_components()?;
1728
1729 let err = token_provider.token().await.unwrap_err();
1730 assert!(!err.is_transient());
1731
1732 server.verify_and_clear();
1733 Ok(())
1734 }
1735}