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 find_source_error, get_mock_auth_retry_policy, get_mock_backoff_policy,
643 get_mock_retry_throttler,
644 };
645 use crate::errors::CredentialsError;
646 use httptest::cycle;
647 use httptest::{Expectation, Server, matchers::*, responders::*};
648 use serde_json::json;
649
650 type TestResult = anyhow::Result<()>;
651
652 #[tokio::test]
653 async fn test_generate_access_token_success() -> TestResult {
654 let server = Server::run();
655 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
656 .format(&time::format_description::well_known::Rfc3339)
657 .unwrap();
658 server.expect(
659 Expectation::matching(all_of![
660 request::method_path(
661 "POST",
662 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
663 ),
664 request::headers(contains(("authorization", "Bearer test-token"))),
665 ])
666 .respond_with(json_encoded(json!({
667 "accessToken": "test-impersonated-token",
668 "expireTime": expire_time
669 }))),
670 );
671
672 let mut headers = HeaderMap::new();
673 headers.insert("authorization", "Bearer test-token".parse().unwrap());
674 let token = generate_access_token(
675 headers,
676 None,
677 vec!["scope".to_string()],
678 DEFAULT_LIFETIME,
679 &server
680 .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
681 .to_string(),
682 )
683 .await?;
684
685 assert_eq!(token.token, "test-impersonated-token");
686 Ok(())
687 }
688
689 #[tokio::test]
690 async fn test_generate_access_token_403() -> TestResult {
691 let server = Server::run();
692 server.expect(
693 Expectation::matching(all_of![
694 request::method_path(
695 "POST",
696 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
697 ),
698 request::headers(contains(("authorization", "Bearer test-token"))),
699 ])
700 .respond_with(status_code(403)),
701 );
702
703 let mut headers = HeaderMap::new();
704 headers.insert("authorization", "Bearer test-token".parse().unwrap());
705 let err = generate_access_token(
706 headers,
707 None,
708 vec!["scope".to_string()],
709 DEFAULT_LIFETIME,
710 &server
711 .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
712 .to_string(),
713 )
714 .await
715 .unwrap_err();
716
717 assert!(!err.is_transient());
718 Ok(())
719 }
720
721 #[tokio::test]
722 async fn test_generate_access_token_no_auth_header() -> TestResult {
723 let server = Server::run();
724 server.expect(
725 Expectation::matching(request::method_path(
726 "POST",
727 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
728 ))
729 .respond_with(status_code(401)),
730 );
731
732 let err = generate_access_token(
733 HeaderMap::new(),
734 None,
735 vec!["scope".to_string()],
736 DEFAULT_LIFETIME,
737 &server
738 .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
739 .to_string(),
740 )
741 .await
742 .unwrap_err();
743
744 assert!(!err.is_transient());
745 Ok(())
746 }
747
748 #[tokio::test]
749 async fn test_impersonated_service_account() -> TestResult {
750 let server = Server::run();
751 server.expect(
752 Expectation::matching(request::method_path("POST", "/token")).respond_with(
753 json_encoded(json!({
754 "access_token": "test-user-account-token",
755 "expires_in": 3600,
756 "token_type": "Bearer",
757 })),
758 ),
759 );
760 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
761 .format(&time::format_description::well_known::Rfc3339)
762 .unwrap();
763 server.expect(
764 Expectation::matching(all_of![
765 request::method_path(
766 "POST",
767 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
768 ),
769 request::headers(contains((
770 "authorization",
771 "Bearer test-user-account-token"
772 ))),
773 request::body(json_decoded(eq(json!({
774 "scope": ["scope1", "scope2"],
775 "lifetime": "3600s"
776 }))))
777 ])
778 .respond_with(json_encoded(json!({
779 "accessToken": "test-impersonated-token",
780 "expireTime": expire_time
781 }))),
782 );
783
784 let impersonated_credential = json!({
785 "type": "impersonated_service_account",
786 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
787 "source_credentials": {
788 "type": "authorized_user",
789 "client_id": "test-client-id",
790 "client_secret": "test-client-secret",
791 "refresh_token": "test-refresh-token",
792 "token_uri": server.url("/token").to_string()
793 }
794 });
795 let (token_provider, _) = Builder::new(impersonated_credential)
796 .with_scopes(vec!["scope1", "scope2"])
797 .build_components()?;
798
799 let token = token_provider.token().await?;
800 assert_eq!(token.token, "test-impersonated-token");
801 assert_eq!(token.token_type, "Bearer");
802
803 Ok(())
804 }
805
806 #[tokio::test]
807 async fn test_impersonated_service_account_default_scope() -> TestResult {
808 let server = Server::run();
809 server.expect(
810 Expectation::matching(request::method_path("POST", "/token")).respond_with(
811 json_encoded(json!({
812 "access_token": "test-user-account-token",
813 "expires_in": 3600,
814 "token_type": "Bearer",
815 })),
816 ),
817 );
818 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
819 .format(&time::format_description::well_known::Rfc3339)
820 .unwrap();
821 server.expect(
822 Expectation::matching(all_of![
823 request::method_path(
824 "POST",
825 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
826 ),
827 request::headers(contains((
828 "authorization",
829 "Bearer test-user-account-token"
830 ))),
831 request::body(json_decoded(eq(json!({
832 "scope": [DEFAULT_SCOPE],
833 "lifetime": "3600s"
834 }))))
835 ])
836 .respond_with(json_encoded(json!({
837 "accessToken": "test-impersonated-token",
838 "expireTime": expire_time
839 }))),
840 );
841
842 let impersonated_credential = json!({
843 "type": "impersonated_service_account",
844 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
845 "source_credentials": {
846 "type": "authorized_user",
847 "client_id": "test-client-id",
848 "client_secret": "test-client-secret",
849 "refresh_token": "test-refresh-token",
850 "token_uri": server.url("/token").to_string()
851 }
852 });
853 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
854
855 let token = token_provider.token().await?;
856 assert_eq!(token.token, "test-impersonated-token");
857 assert_eq!(token.token_type, "Bearer");
858
859 Ok(())
860 }
861
862 #[tokio::test]
863 async fn test_impersonated_service_account_with_custom_lifetime() -> TestResult {
864 let server = Server::run();
865 server.expect(
866 Expectation::matching(request::method_path("POST", "/token")).respond_with(
867 json_encoded(json!({
868 "access_token": "test-user-account-token",
869 "expires_in": 3600,
870 "token_type": "Bearer",
871 })),
872 ),
873 );
874 let expire_time = (OffsetDateTime::now_utc() + time::Duration::seconds(500))
875 .format(&time::format_description::well_known::Rfc3339)
876 .unwrap();
877 server.expect(
878 Expectation::matching(all_of![
879 request::method_path(
880 "POST",
881 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
882 ),
883 request::headers(contains((
884 "authorization",
885 "Bearer test-user-account-token"
886 ))),
887 request::body(json_decoded(eq(json!({
888 "scope": ["scope1", "scope2"],
889 "lifetime": "3.5s"
890 }))))
891 ])
892 .respond_with(json_encoded(json!({
893 "accessToken": "test-impersonated-token",
894 "expireTime": expire_time
895 }))),
896 );
897
898 let impersonated_credential = json!({
899 "type": "impersonated_service_account",
900 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
901 "source_credentials": {
902 "type": "authorized_user",
903 "client_id": "test-client-id",
904 "client_secret": "test-client-secret",
905 "refresh_token": "test-refresh-token",
906 "token_uri": server.url("/token").to_string()
907 }
908 });
909 let (token_provider, _) = Builder::new(impersonated_credential)
910 .with_scopes(vec!["scope1", "scope2"])
911 .with_lifetime(Duration::from_secs_f32(3.5))
912 .build_components()?;
913
914 let token = token_provider.token().await?;
915 assert_eq!(token.token, "test-impersonated-token");
916
917 Ok(())
918 }
919
920 #[tokio::test]
921 async fn test_with_delegates() -> TestResult {
922 let server = Server::run();
923 server.expect(
924 Expectation::matching(request::method_path("POST", "/token")).respond_with(
925 json_encoded(json!({
926 "access_token": "test-user-account-token",
927 "expires_in": 3600,
928 "token_type": "Bearer",
929 })),
930 ),
931 );
932 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
933 .format(&time::format_description::well_known::Rfc3339)
934 .unwrap();
935 server.expect(
936 Expectation::matching(all_of![
937 request::method_path(
938 "POST",
939 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
940 ),
941 request::headers(contains((
942 "authorization",
943 "Bearer test-user-account-token"
944 ))),
945 request::body(json_decoded(eq(json!({
946 "scope": [DEFAULT_SCOPE],
947 "lifetime": "3600s",
948 "delegates": ["delegate1", "delegate2"]
949 }))))
950 ])
951 .respond_with(json_encoded(json!({
952 "accessToken": "test-impersonated-token",
953 "expireTime": expire_time
954 }))),
955 );
956
957 let impersonated_credential = json!({
958 "type": "impersonated_service_account",
959 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
960 "source_credentials": {
961 "type": "authorized_user",
962 "client_id": "test-client-id",
963 "client_secret": "test-client-secret",
964 "refresh_token": "test-refresh-token",
965 "token_uri": server.url("/token").to_string()
966 }
967 });
968 let (token_provider, _) = Builder::new(impersonated_credential)
969 .with_delegates(vec!["delegate1", "delegate2"])
970 .build_components()?;
971
972 let token = token_provider.token().await?;
973 assert_eq!(token.token, "test-impersonated-token");
974 assert_eq!(token.token_type, "Bearer");
975
976 Ok(())
977 }
978
979 #[tokio::test]
980 async fn test_impersonated_service_account_fail() -> TestResult {
981 let server = Server::run();
982 server.expect(
983 Expectation::matching(request::method_path("POST", "/token")).respond_with(
984 json_encoded(json!({
985 "access_token": "test-user-account-token",
986 "expires_in": 3600,
987 "token_type": "Bearer",
988 })),
989 ),
990 );
991 server.expect(
992 Expectation::matching(request::method_path(
993 "POST",
994 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
995 ))
996 .respond_with(status_code(500)),
997 );
998
999 let impersonated_credential = json!({
1000 "type": "impersonated_service_account",
1001 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1002 "source_credentials": {
1003 "type": "authorized_user",
1004 "client_id": "test-client-id",
1005 "client_secret": "test-client-secret",
1006 "refresh_token": "test-refresh-token",
1007 "token_uri": server.url("/token").to_string()
1008 }
1009 });
1010 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1011
1012 let err = token_provider.token().await.unwrap_err();
1013 let original_err = find_source_error::<CredentialsError>(&err).unwrap();
1014 assert!(original_err.is_transient());
1015
1016 Ok(())
1017 }
1018
1019 #[tokio::test]
1020 async fn debug_token_provider() {
1021 let source_credentials = crate::credentials::user_account::Builder::new(json!({
1022 "type": "authorized_user",
1023 "client_id": "test-client-id",
1024 "client_secret": "test-client-secret",
1025 "refresh_token": "test-refresh-token"
1026 }))
1027 .build()
1028 .unwrap();
1029
1030 let expected = ImpersonatedTokenProvider {
1031 source_credentials,
1032 service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1033 delegates: Some(vec!["delegate1".to_string()]),
1034 scopes: vec!["scope1".to_string()],
1035 lifetime: Duration::from_secs(3600),
1036 };
1037 let fmt = format!("{expected:?}");
1038 assert!(fmt.contains("UserCredentials"), "{fmt}");
1039 assert!(fmt.contains("test-client-id"), "{fmt}");
1040 assert!(!fmt.contains("test-client-secret"), "{fmt}");
1041 assert!(!fmt.contains("test-refresh-token"), "{fmt}");
1042 assert!(fmt.contains("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"), "{fmt}");
1043 assert!(fmt.contains("delegate1"), "{fmt}");
1044 assert!(fmt.contains("scope1"), "{fmt}");
1045 assert!(fmt.contains("3600s"), "{fmt}");
1046 }
1047
1048 #[test]
1049 fn impersonated_config_full_from_json_success() {
1050 let source_credentials_json = json!({
1051 "type": "authorized_user",
1052 "client_id": "test-client-id",
1053 "client_secret": "test-client-secret",
1054 "refresh_token": "test-refresh-token"
1055 });
1056 let json = json!({
1057 "type": "impersonated_service_account",
1058 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1059 "source_credentials": source_credentials_json,
1060 "delegates": ["delegate1"],
1061 "quota_project_id": "test-project-id",
1062 });
1063
1064 let expected = ImpersonatedConfig {
1065 service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1066 source_credentials: source_credentials_json,
1067 delegates: Some(vec!["delegate1".to_string()]),
1068 quota_project_id: Some("test-project-id".to_string()),
1069 };
1070 let actual: ImpersonatedConfig = serde_json::from_value(json).unwrap();
1071 assert_eq!(actual, expected);
1072 }
1073
1074 #[test]
1075 fn impersonated_config_partial_from_json_success() {
1076 let source_credentials_json = json!({
1077 "type": "authorized_user",
1078 "client_id": "test-client-id",
1079 "client_secret": "test-client-secret",
1080 "refresh_token": "test-refresh-token"
1081 });
1082 let json = json!({
1083 "type": "impersonated_service_account",
1084 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1085 "source_credentials": source_credentials_json
1086 });
1087
1088 let config: ImpersonatedConfig = serde_json::from_value(json).unwrap();
1089 assert_eq!(
1090 config.service_account_impersonation_url,
1091 "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1092 );
1093 assert_eq!(config.source_credentials, source_credentials_json);
1094 assert_eq!(config.delegates, None);
1095 assert_eq!(config.quota_project_id, None);
1096 }
1097
1098 #[tokio::test]
1099 async fn test_impersonated_service_account_source_fail() -> TestResult {
1100 #[derive(Debug)]
1101 struct MockSourceCredentialsFail;
1102
1103 #[async_trait]
1104 impl CredentialsProvider for MockSourceCredentialsFail {
1105 async fn headers(
1106 &self,
1107 _extensions: Extensions,
1108 ) -> Result<CacheableResource<HeaderMap>> {
1109 Err(errors::non_retryable_from_str("source failed"))
1110 }
1111 }
1112
1113 let source_credentials = Credentials {
1114 inner: Arc::new(MockSourceCredentialsFail),
1115 };
1116
1117 let token_provider = ImpersonatedTokenProvider {
1118 source_credentials,
1119 service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1120 delegates: Some(vec!["delegate1".to_string()]),
1121 scopes: vec!["scope1".to_string()],
1122 lifetime: DEFAULT_LIFETIME,
1123 };
1124
1125 let err = token_provider.token().await.unwrap_err();
1126 assert!(err.to_string().contains("source failed"));
1127
1128 Ok(())
1129 }
1130
1131 #[tokio::test]
1132 async fn test_missing_impersonation_url_fail() {
1133 let source_credentials = crate::credentials::user_account::Builder::new(json!({
1134 "type": "authorized_user",
1135 "client_id": "test-client-id",
1136 "client_secret": "test-client-secret",
1137 "refresh_token": "test-refresh-token"
1138 }))
1139 .build()
1140 .unwrap();
1141
1142 let result = Builder::from_source_credentials(source_credentials).build();
1143 assert!(result.is_err());
1144 let err = result.unwrap_err();
1145 assert!(err.is_parsing());
1146 assert!(
1147 err.to_string()
1148 .contains("`service_account_impersonation_url` is required")
1149 );
1150 }
1151
1152 #[tokio::test]
1153 async fn test_nested_impersonated_credentials_fail() {
1154 let nested_impersonated = json!({
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": "impersonated_service_account",
1159 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1160 "source_credentials": {
1161 "type": "authorized_user",
1162 "client_id": "test-client-id",
1163 "client_secret": "test-client-secret",
1164 "refresh_token": "test-refresh-token"
1165 }
1166 }
1167 });
1168
1169 let result = Builder::new(nested_impersonated).build();
1170 assert!(result.is_err());
1171 let err = result.unwrap_err();
1172 assert!(err.is_parsing());
1173 assert!(
1174 err.to_string().contains(
1175 "source credential of type `impersonated_service_account` is not supported"
1176 )
1177 );
1178 }
1179
1180 #[tokio::test]
1181 async fn test_malformed_impersonated_credentials_fail() {
1182 let malformed_impersonated = json!({
1183 "type": "impersonated_service_account",
1184 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1185 });
1186
1187 let result = Builder::new(malformed_impersonated).build();
1188 assert!(result.is_err());
1189 let err = result.unwrap_err();
1190 assert!(err.is_parsing());
1191 assert!(
1192 err.to_string()
1193 .contains("missing field `source_credentials`")
1194 );
1195 }
1196
1197 #[tokio::test]
1198 async fn test_invalid_source_credential_type_fail() {
1199 let invalid_source = json!({
1200 "type": "impersonated_service_account",
1201 "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1202 "source_credentials": {
1203 "type": "invalid_type",
1204 }
1205 });
1206
1207 let result = Builder::new(invalid_source).build();
1208 assert!(result.is_err());
1209 let err = result.unwrap_err();
1210 assert!(err.is_unknown_type());
1211 }
1212
1213 #[tokio::test]
1214 async fn test_missing_expiry() -> TestResult {
1215 let server = Server::run();
1216 server.expect(
1217 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1218 json_encoded(json!({
1219 "access_token": "test-user-account-token",
1220 "expires_in": 3600,
1221 "token_type": "Bearer",
1222 })),
1223 ),
1224 );
1225 server.expect(
1226 Expectation::matching(request::method_path(
1227 "POST",
1228 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1229 ))
1230 .respond_with(json_encoded(json!({
1231 "accessToken": "test-impersonated-token",
1232 }))),
1233 );
1234
1235 let impersonated_credential = json!({
1236 "type": "impersonated_service_account",
1237 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1238 "source_credentials": {
1239 "type": "authorized_user",
1240 "client_id": "test-client-id",
1241 "client_secret": "test-client-secret",
1242 "refresh_token": "test-refresh-token",
1243 "token_uri": server.url("/token").to_string()
1244 }
1245 });
1246 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1247
1248 let err = token_provider.token().await.unwrap_err();
1249 assert!(!err.is_transient());
1250
1251 Ok(())
1252 }
1253
1254 #[tokio::test]
1255 async fn test_invalid_expiry_format() -> TestResult {
1256 let server = Server::run();
1257 server.expect(
1258 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1259 json_encoded(json!({
1260 "access_token": "test-user-account-token",
1261 "expires_in": 3600,
1262 "token_type": "Bearer",
1263 })),
1264 ),
1265 );
1266 server.expect(
1267 Expectation::matching(request::method_path(
1268 "POST",
1269 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1270 ))
1271 .respond_with(json_encoded(json!({
1272 "accessToken": "test-impersonated-token",
1273 "expireTime": "invalid-format"
1274 }))),
1275 );
1276
1277 let impersonated_credential = json!({
1278 "type": "impersonated_service_account",
1279 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1280 "source_credentials": {
1281 "type": "authorized_user",
1282 "client_id": "test-client-id",
1283 "client_secret": "test-client-secret",
1284 "refresh_token": "test-refresh-token",
1285 "token_uri": server.url("/token").to_string()
1286 }
1287 });
1288 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1289
1290 let err = token_provider.token().await.unwrap_err();
1291 assert!(!err.is_transient());
1292
1293 Ok(())
1294 }
1295
1296 #[tokio::test]
1297 async fn token_provider_malformed_response_is_nonretryable() -> TestResult {
1298 let server = Server::run();
1299 server.expect(
1300 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1301 json_encoded(json!({
1302 "access_token": "test-user-account-token",
1303 "expires_in": 3600,
1304 "token_type": "Bearer",
1305 })),
1306 ),
1307 );
1308 server.expect(
1309 Expectation::matching(request::method_path(
1310 "POST",
1311 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1312 ))
1313 .respond_with(json_encoded(json!("bad json"))),
1314 );
1315
1316 let impersonated_credential = json!({
1317 "type": "impersonated_service_account",
1318 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1319 "source_credentials": {
1320 "type": "authorized_user",
1321 "client_id": "test-client-id",
1322 "client_secret": "test-client-secret",
1323 "refresh_token": "test-refresh-token",
1324 "token_uri": server.url("/token").to_string()
1325 }
1326 });
1327 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1328
1329 let e = token_provider.token().await.err().unwrap();
1330 assert!(!e.is_transient(), "{e}");
1331
1332 Ok(())
1333 }
1334
1335 #[tokio::test]
1336 async fn token_provider_nonretryable_error() -> TestResult {
1337 let server = Server::run();
1338 server.expect(
1339 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1340 json_encoded(json!({
1341 "access_token": "test-user-account-token",
1342 "expires_in": 3600,
1343 "token_type": "Bearer",
1344 })),
1345 ),
1346 );
1347 server.expect(
1348 Expectation::matching(request::method_path(
1349 "POST",
1350 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1351 ))
1352 .respond_with(status_code(401)),
1353 );
1354
1355 let impersonated_credential = json!({
1356 "type": "impersonated_service_account",
1357 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1358 "source_credentials": {
1359 "type": "authorized_user",
1360 "client_id": "test-client-id",
1361 "client_secret": "test-client-secret",
1362 "refresh_token": "test-refresh-token",
1363 "token_uri": server.url("/token").to_string()
1364 }
1365 });
1366 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1367
1368 let err = token_provider.token().await.unwrap_err();
1369 assert!(!err.is_transient());
1370
1371 Ok(())
1372 }
1373
1374 #[tokio::test]
1375 async fn credential_full_with_quota_project_from_builder() -> TestResult {
1376 let server = Server::run();
1377 server.expect(
1378 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1379 json_encoded(json!({
1380 "access_token": "test-user-account-token",
1381 "expires_in": 3600,
1382 "token_type": "Bearer",
1383 })),
1384 ),
1385 );
1386 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1387 .format(&time::format_description::well_known::Rfc3339)
1388 .unwrap();
1389 server.expect(
1390 Expectation::matching(request::method_path(
1391 "POST",
1392 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1393 ))
1394 .respond_with(json_encoded(json!({
1395 "accessToken": "test-impersonated-token",
1396 "expireTime": expire_time
1397 }))),
1398 );
1399
1400 let impersonated_credential = json!({
1401 "type": "impersonated_service_account",
1402 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1403 "source_credentials": {
1404 "type": "authorized_user",
1405 "client_id": "test-client-id",
1406 "client_secret": "test-client-secret",
1407 "refresh_token": "test-refresh-token",
1408 "token_uri": server.url("/token").to_string()
1409 }
1410 });
1411 let creds = Builder::new(impersonated_credential)
1412 .with_quota_project_id("test-project")
1413 .build()?;
1414
1415 let headers = creds.headers(Extensions::new()).await?;
1416 match headers {
1417 CacheableResource::New { data, .. } => {
1418 assert_eq!(data.get("x-goog-user-project").unwrap(), "test-project");
1419 }
1420 CacheableResource::NotModified => panic!("Expected new headers, but got NotModified"),
1421 }
1422
1423 Ok(())
1424 }
1425
1426 #[tokio::test]
1427 async fn test_with_target_principal() {
1428 let source_credentials = crate::credentials::user_account::Builder::new(json!({
1429 "type": "authorized_user",
1430 "client_id": "test-client-id",
1431 "client_secret": "test-client-secret",
1432 "refresh_token": "test-refresh-token"
1433 }))
1434 .build()
1435 .unwrap();
1436
1437 let (token_provider, _) = Builder::from_source_credentials(source_credentials)
1438 .with_target_principal("test-principal@example.iam.gserviceaccount.com")
1439 .build_components()
1440 .unwrap();
1441
1442 assert_eq!(
1443 token_provider.inner.service_account_impersonation_url,
1444 "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal@example.iam.gserviceaccount.com:generateAccessToken"
1445 );
1446 }
1447
1448 #[tokio::test]
1449 async fn credential_full_with_quota_project_from_json() -> TestResult {
1450 let server = Server::run();
1451 server.expect(
1452 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1453 json_encoded(json!({
1454 "access_token": "test-user-account-token",
1455 "expires_in": 3600,
1456 "token_type": "Bearer",
1457 })),
1458 ),
1459 );
1460 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1461 .format(&time::format_description::well_known::Rfc3339)
1462 .unwrap();
1463 server.expect(
1464 Expectation::matching(request::method_path(
1465 "POST",
1466 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1467 ))
1468 .respond_with(json_encoded(json!({
1469 "accessToken": "test-impersonated-token",
1470 "expireTime": expire_time
1471 }))),
1472 );
1473
1474 let impersonated_credential = json!({
1475 "type": "impersonated_service_account",
1476 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1477 "source_credentials": {
1478 "type": "authorized_user",
1479 "client_id": "test-client-id",
1480 "client_secret": "test-client-secret",
1481 "refresh_token": "test-refresh-token",
1482 "token_uri": server.url("/token").to_string()
1483 },
1484 "quota_project_id": "test-project-from-json",
1485 });
1486
1487 let creds = Builder::new(impersonated_credential).build()?;
1488
1489 let headers = creds.headers(Extensions::new()).await?;
1490 match headers {
1491 CacheableResource::New { data, .. } => {
1492 assert_eq!(
1493 data.get("x-goog-user-project").unwrap(),
1494 "test-project-from-json"
1495 );
1496 }
1497 CacheableResource::NotModified => panic!("Expected new headers, but got NotModified"),
1498 }
1499
1500 Ok(())
1501 }
1502
1503 #[tokio::test]
1504 async fn test_impersonated_does_not_propagate_settings_to_source() -> TestResult {
1505 let server = Server::run();
1506
1507 server.expect(
1510 Expectation::matching(all_of![
1511 request::method_path("POST", "/source_token"),
1512 request::body(json_decoded(
1513 |body: &serde_json::Value| body["scopes"].is_null()
1514 ))
1515 ])
1516 .respond_with(json_encoded(json!({
1517 "access_token": "source-token",
1518 "expires_in": 3600,
1519 "token_type": "Bearer",
1520 }))),
1521 );
1522
1523 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1524 .format(&time::format_description::well_known::Rfc3339)
1525 .unwrap();
1526
1527 server.expect(
1530 Expectation::matching(all_of![
1531 request::method_path(
1532 "POST",
1533 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1534 ),
1535 request::headers(contains(("authorization", "Bearer source-token"))),
1536 request::body(json_decoded(eq(json!({
1537 "scope": ["impersonated-scope"],
1538 "lifetime": "3600s"
1539 }))))
1540 ])
1541 .respond_with(json_encoded(json!({
1542 "accessToken": "impersonated-token",
1543 "expireTime": expire_time
1544 }))),
1545 );
1546
1547 let impersonated_credential = json!({
1548 "type": "impersonated_service_account",
1549 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1550 "source_credentials": {
1551 "type": "authorized_user",
1552 "client_id": "test-client-id",
1553 "client_secret": "test-client-secret",
1554 "refresh_token": "test-refresh-token",
1555 "token_uri": server.url("/source_token").to_string()
1556 }
1557 });
1558
1559 let creds = Builder::new(impersonated_credential)
1560 .with_scopes(vec!["impersonated-scope"])
1561 .with_quota_project_id("impersonated-quota-project")
1562 .build()?;
1563
1564 let fmt = format!("{creds:?}");
1566 assert!(fmt.contains("impersonated-quota-project"));
1567
1568 let _token = creds.headers(Extensions::new()).await?;
1570
1571 Ok(())
1572 }
1573
1574 #[tokio::test]
1575 async fn test_impersonated_metrics_header() -> TestResult {
1576 let server = Server::run();
1577 server.expect(
1578 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1579 json_encoded(json!({
1580 "access_token": "test-user-account-token",
1581 "expires_in": 3600,
1582 "token_type": "Bearer",
1583 })),
1584 ),
1585 );
1586 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1587 .format(&time::format_description::well_known::Rfc3339)
1588 .unwrap();
1589 server.expect(
1590 Expectation::matching(all_of![
1591 request::method_path(
1592 "POST",
1593 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1594 ),
1595 request::headers(contains(("x-goog-api-client", matches("cred-type/imp")))),
1596 request::headers(contains((
1597 "x-goog-api-client",
1598 matches("auth-request-type/at")
1599 )))
1600 ])
1601 .respond_with(json_encoded(json!({
1602 "accessToken": "test-impersonated-token",
1603 "expireTime": expire_time
1604 }))),
1605 );
1606
1607 let impersonated_credential = json!({
1608 "type": "impersonated_service_account",
1609 "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1610 "source_credentials": {
1611 "type": "authorized_user",
1612 "client_id": "test-client-id",
1613 "client_secret": "test-client-secret",
1614 "refresh_token": "test-refresh-token",
1615 "token_uri": server.url("/token").to_string()
1616 }
1617 });
1618 let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1619
1620 let token = token_provider.token().await?;
1621 assert_eq!(token.token, "test-impersonated-token");
1622 assert_eq!(token.token_type, "Bearer");
1623
1624 Ok(())
1625 }
1626
1627 #[tokio::test]
1628 async fn test_impersonated_retries_for_success() -> TestResult {
1629 let mut server = Server::run();
1630 server.expect(
1632 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1633 json_encoded(json!({
1634 "access_token": "test-user-account-token",
1635 "expires_in": 3600,
1636 "token_type": "Bearer",
1637 })),
1638 ),
1639 );
1640
1641 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1642 .format(&time::format_description::well_known::Rfc3339)
1643 .unwrap();
1644
1645 let impersonation_path =
1647 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
1648 server.expect(
1649 Expectation::matching(request::method_path("POST", impersonation_path))
1650 .times(3)
1651 .respond_with(cycle![
1652 status_code(503).body("try-again"),
1653 status_code(503).body("try-again"),
1654 status_code(200)
1655 .append_header("Content-Type", "application/json")
1656 .body(
1657 json!({
1658 "accessToken": "test-impersonated-token",
1659 "expireTime": expire_time
1660 })
1661 .to_string()
1662 ),
1663 ]),
1664 );
1665
1666 let impersonated_credential = json!({
1667 "type": "impersonated_service_account",
1668 "service_account_impersonation_url": server.url(impersonation_path).to_string(),
1669 "source_credentials": {
1670 "type": "authorized_user",
1671 "client_id": "test-client-id",
1672 "client_secret": "test-client-secret",
1673 "refresh_token": "test-refresh-token",
1674 "token_uri": server.url("/token").to_string()
1675 }
1676 });
1677
1678 let (token_provider, _) = Builder::new(impersonated_credential)
1679 .with_retry_policy(get_mock_auth_retry_policy(3))
1680 .with_backoff_policy(get_mock_backoff_policy())
1681 .with_retry_throttler(get_mock_retry_throttler())
1682 .build_components()?;
1683
1684 let token = token_provider.token().await?;
1685 assert_eq!(token.token, "test-impersonated-token");
1686
1687 server.verify_and_clear();
1688 Ok(())
1689 }
1690
1691 #[tokio::test]
1692 async fn test_impersonated_does_not_retry_on_non_transient_failures() -> TestResult {
1693 let mut server = Server::run();
1694 server.expect(
1696 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1697 json_encoded(json!({
1698 "access_token": "test-user-account-token",
1699 "expires_in": 3600,
1700 "token_type": "Bearer",
1701 })),
1702 ),
1703 );
1704
1705 let impersonation_path =
1707 "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
1708 server.expect(
1709 Expectation::matching(request::method_path("POST", impersonation_path))
1710 .times(1)
1711 .respond_with(status_code(401)),
1712 );
1713
1714 let impersonated_credential = json!({
1715 "type": "impersonated_service_account",
1716 "service_account_impersonation_url": server.url(impersonation_path).to_string(),
1717 "source_credentials": {
1718 "type": "authorized_user",
1719 "client_id": "test-client-id",
1720 "client_secret": "test-client-secret",
1721 "refresh_token": "test-refresh-token",
1722 "token_uri": server.url("/token").to_string()
1723 }
1724 });
1725
1726 let (token_provider, _) = Builder::new(impersonated_credential)
1727 .with_retry_policy(get_mock_auth_retry_policy(3))
1728 .with_backoff_policy(get_mock_backoff_policy())
1729 .with_retry_throttler(get_mock_retry_throttler())
1730 .build_components()?;
1731
1732 let err = token_provider.token().await.unwrap_err();
1733 assert!(!err.is_transient());
1734
1735 server.verify_and_clear();
1736 Ok(())
1737 }
1738}