1use crate::build_errors::Error as BuilderError;
16use crate::constants::GOOGLE_CLOUD_QUOTA_PROJECT_VAR;
17use crate::errors::{self, CredentialsError};
18use crate::{BuildResult, Result};
19use http::{Extensions, HeaderMap};
20use serde_json::Value;
21use std::future::Future;
22use std::sync::Arc;
23use std::sync::atomic::{AtomicU64, Ordering};
24
25pub mod anonymous;
26pub mod api_key_credentials;
27pub mod external_account;
28pub(crate) mod external_account_sources;
29pub mod impersonated;
30pub(crate) mod internal;
31pub mod mds;
32pub mod service_account;
33pub mod subject_token;
34pub mod user_account;
35pub(crate) const QUOTA_PROJECT_KEY: &str = "x-goog-user-project";
36
37#[cfg(test)]
38pub(crate) const DEFAULT_UNIVERSE_DOMAIN: &str = "googleapis.com";
39
40#[derive(Clone, Debug, PartialEq, Default)]
50pub struct EntityTag(u64);
51
52static ENTITY_TAG_GENERATOR: AtomicU64 = AtomicU64::new(0);
53impl EntityTag {
54 pub fn new() -> Self {
55 let value = ENTITY_TAG_GENERATOR.fetch_add(1, Ordering::SeqCst);
56 Self(value)
57 }
58}
59
60#[derive(Clone, PartialEq, Debug)]
67pub enum CacheableResource<T> {
68 NotModified,
69 New { entity_tag: EntityTag, data: T },
70}
71
72#[derive(Clone, Debug)]
105pub struct Credentials {
106 inner: Arc<dyn dynamic::CredentialsProvider>,
115}
116
117impl<T> std::convert::From<T> for Credentials
118where
119 T: crate::credentials::CredentialsProvider + Send + Sync + 'static,
120{
121 fn from(value: T) -> Self {
122 Self {
123 inner: Arc::new(value),
124 }
125 }
126}
127
128impl Credentials {
129 pub async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
130 self.inner.headers(extensions).await
131 }
132
133 pub async fn universe_domain(&self) -> Option<String> {
134 self.inner.universe_domain().await
135 }
136}
137
138pub trait CredentialsProvider: std::fmt::Debug {
178 fn headers(
200 &self,
201 extensions: Extensions,
202 ) -> impl Future<Output = Result<CacheableResource<HeaderMap>>> + Send;
203
204 fn universe_domain(&self) -> impl Future<Output = Option<String>> + Send;
206}
207
208pub(crate) mod dynamic {
209 use super::Result;
210 use super::{CacheableResource, Extensions, HeaderMap};
211
212 #[async_trait::async_trait]
214 pub trait CredentialsProvider: Send + Sync + std::fmt::Debug {
215 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>>;
237
238 async fn universe_domain(&self) -> Option<String> {
240 Some("googleapis.com".to_string())
241 }
242 }
243
244 #[async_trait::async_trait]
246 impl<T> CredentialsProvider for T
247 where
248 T: super::CredentialsProvider + Send + Sync,
249 {
250 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
251 T::headers(self, extensions).await
252 }
253 async fn universe_domain(&self) -> Option<String> {
254 T::universe_domain(self).await
255 }
256 }
257}
258
259#[derive(Debug)]
314pub struct Builder {
315 quota_project_id: Option<String>,
316 scopes: Option<Vec<String>>,
317}
318
319impl Default for Builder {
320 fn default() -> Self {
332 Self {
333 quota_project_id: None,
334 scopes: None,
335 }
336 }
337}
338
339impl Builder {
340 pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
362 self.quota_project_id = Some(quota_project_id.into());
363 self
364 }
365
366 pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
386 where
387 I: IntoIterator<Item = S>,
388 S: Into<String>,
389 {
390 self.scopes = Some(scopes.into_iter().map(|s| s.into()).collect());
391 self
392 }
393
394 pub fn build(self) -> BuildResult<Credentials> {
405 let json_data = match load_adc()? {
406 AdcContents::Contents(contents) => {
407 Some(serde_json::from_str(&contents).map_err(BuilderError::parsing)?)
408 }
409 AdcContents::FallbackToMds => None,
410 };
411 let quota_project_id = std::env::var(GOOGLE_CLOUD_QUOTA_PROJECT_VAR)
412 .ok()
413 .or(self.quota_project_id);
414 build_credentials(json_data, quota_project_id, self.scopes)
415 }
416}
417
418#[derive(Debug, PartialEq)]
419enum AdcPath {
420 FromEnv(String),
421 WellKnown(String),
422}
423
424#[derive(Debug, PartialEq)]
425enum AdcContents {
426 Contents(String),
427 FallbackToMds,
428}
429
430fn extract_credential_type(json: &Value) -> BuildResult<&str> {
431 json.get("type")
432 .ok_or_else(|| BuilderError::parsing("no `type` field found."))?
433 .as_str()
434 .ok_or_else(|| BuilderError::parsing("`type` field is not a string."))
435}
436
437macro_rules! config_builder {
445 ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $apply_scopes_closure:expr) => {{
446 let builder = $builder_instance;
447 let builder = $quota_project_id_option
448 .into_iter()
449 .fold(builder, |b, qp| b.with_quota_project_id(qp));
450
451 let builder = $scopes_option
452 .into_iter()
453 .fold(builder, |b, s| $apply_scopes_closure(b, s));
454
455 builder.build()
456 }};
457}
458
459fn build_credentials(
460 json: Option<Value>,
461 quota_project_id: Option<String>,
462 scopes: Option<Vec<String>>,
463) -> BuildResult<Credentials> {
464 match json {
465 None => config_builder!(
466 mds::Builder::from_adc(),
467 quota_project_id,
468 scopes,
469 |b: mds::Builder, s: Vec<String>| b.with_scopes(s)
470 ),
471 Some(json) => {
472 let cred_type = extract_credential_type(&json)?;
473 match cred_type {
474 "authorized_user" => {
475 config_builder!(
476 user_account::Builder::new(json),
477 quota_project_id,
478 scopes,
479 |b: user_account::Builder, s: Vec<String>| b.with_scopes(s)
480 )
481 }
482 "service_account" => config_builder!(
483 service_account::Builder::new(json),
484 quota_project_id,
485 scopes,
486 |b: service_account::Builder, s: Vec<String>| b
487 .with_access_specifier(service_account::AccessSpecifier::from_scopes(s))
488 ),
489 "impersonated_service_account" => {
490 config_builder!(
491 impersonated::Builder::new(json),
492 quota_project_id,
493 scopes,
494 |b: impersonated::Builder, s: Vec<String>| b.with_scopes(s)
495 )
496 }
497 "external_account" => config_builder!(
498 external_account::Builder::new(json),
499 quota_project_id,
500 scopes,
501 |b: external_account::Builder, s: Vec<String>| b.with_scopes(s)
502 ),
503 _ => Err(BuilderError::unknown_type(cred_type)),
504 }
505 }
506 }
507}
508
509fn path_not_found(path: String) -> BuilderError {
510 BuilderError::loading(format!(
511 "{path}. {}",
512 concat!(
513 "This file name was found in the `GOOGLE_APPLICATION_CREDENTIALS` ",
514 "environment variable. Verify this environment variable points to ",
515 "a valid file."
516 )
517 ))
518}
519
520fn load_adc() -> BuildResult<AdcContents> {
521 match adc_path() {
522 None => Ok(AdcContents::FallbackToMds),
523 Some(AdcPath::FromEnv(path)) => match std::fs::read_to_string(&path) {
524 Ok(contents) => Ok(AdcContents::Contents(contents)),
525 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(path_not_found(path)),
526 Err(e) => Err(BuilderError::loading(e)),
527 },
528 Some(AdcPath::WellKnown(path)) => match std::fs::read_to_string(path) {
529 Ok(contents) => Ok(AdcContents::Contents(contents)),
530 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(AdcContents::FallbackToMds),
531 Err(e) => Err(BuilderError::loading(e)),
532 },
533 }
534}
535
536fn adc_path() -> Option<AdcPath> {
540 if let Ok(path) = std::env::var("GOOGLE_APPLICATION_CREDENTIALS") {
541 return Some(AdcPath::FromEnv(path));
542 }
543 Some(AdcPath::WellKnown(adc_well_known_path()?))
544}
545
546#[cfg(target_os = "windows")]
550fn adc_well_known_path() -> Option<String> {
551 std::env::var("APPDATA")
552 .ok()
553 .map(|root| root + "/gcloud/application_default_credentials.json")
554}
555
556#[cfg(not(target_os = "windows"))]
560fn adc_well_known_path() -> Option<String> {
561 std::env::var("HOME")
562 .ok()
563 .map(|root| root + "/.config/gcloud/application_default_credentials.json")
564}
565
566#[cfg_attr(test, mutants::skip)]
577#[doc(hidden)]
578pub mod testing {
579 use super::CacheableResource;
580 use crate::Result;
581 use crate::credentials::Credentials;
582 use crate::credentials::dynamic::CredentialsProvider;
583 use http::{Extensions, HeaderMap};
584 use std::sync::Arc;
585
586 pub fn error_credentials(retryable: bool) -> Credentials {
590 Credentials {
591 inner: Arc::from(ErrorCredentials(retryable)),
592 }
593 }
594
595 #[derive(Debug, Default)]
596 struct ErrorCredentials(bool);
597
598 #[async_trait::async_trait]
599 impl CredentialsProvider for ErrorCredentials {
600 async fn headers(&self, _extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
601 Err(super::CredentialsError::from_msg(self.0, "test-only"))
602 }
603
604 async fn universe_domain(&self) -> Option<String> {
605 None
606 }
607 }
608}
609
610#[cfg(test)]
611pub(crate) mod tests {
612 use super::*;
613 use base64::Engine;
614 use gax::backoff_policy::BackoffPolicy;
615 use gax::retry_policy::RetryPolicy;
616 use gax::retry_result::RetryResult;
617 use gax::retry_state::RetryState;
618 use gax::retry_throttler::RetryThrottler;
619 use mockall::mock;
620 use num_bigint_dig::BigUint;
621 use reqwest::header::AUTHORIZATION;
622 use rsa::RsaPrivateKey;
623 use rsa::pkcs8::{EncodePrivateKey, LineEnding};
624 use scoped_env::ScopedEnv;
625 use std::error::Error;
626 use std::sync::LazyLock;
627 use test_case::test_case;
628 use tokio::time::Duration;
629
630 pub(crate) fn find_source_error<'a, T: Error + 'static>(
631 error: &'a (dyn Error + 'static),
632 ) -> Option<&'a T> {
633 let mut source = error.source();
634 while let Some(err) = source {
635 if let Some(target_err) = err.downcast_ref::<T>() {
636 return Some(target_err);
637 }
638 source = err.source();
639 }
640 None
641 }
642
643 mock! {
644 #[derive(Debug)]
645 pub RetryPolicy {}
646 impl RetryPolicy for RetryPolicy {
647 fn on_error(
648 &self,
649 state: &RetryState,
650 error: gax::error::Error,
651 ) -> RetryResult;
652 }
653 }
654
655 mock! {
656 #[derive(Debug)]
657 pub BackoffPolicy {}
658 impl BackoffPolicy for BackoffPolicy {
659 fn on_failure(&self, state: &RetryState) -> std::time::Duration;
660 }
661 }
662
663 mockall::mock! {
664 #[derive(Debug)]
665 pub RetryThrottler {}
666 impl RetryThrottler for RetryThrottler {
667 fn throttle_retry_attempt(&self) -> bool;
668 fn on_retry_failure(&mut self, error: &RetryResult);
669 fn on_success(&mut self);
670 }
671 }
672
673 type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
674
675 pub(crate) fn get_mock_auth_retry_policy(attempts: usize) -> MockRetryPolicy {
676 let mut retry_policy = MockRetryPolicy::new();
677 retry_policy
678 .expect_on_error()
679 .returning(move |state, error| {
680 if state.attempt_count >= attempts as u32 {
681 return RetryResult::Exhausted(error);
682 }
683 let is_transient = error
684 .source()
685 .and_then(|e| e.downcast_ref::<CredentialsError>())
686 .is_some_and(|ce| ce.is_transient());
687 if is_transient {
688 RetryResult::Continue(error)
689 } else {
690 RetryResult::Permanent(error)
691 }
692 });
693 retry_policy
694 }
695
696 pub(crate) fn get_mock_backoff_policy() -> MockBackoffPolicy {
697 let mut backoff_policy = MockBackoffPolicy::new();
698 backoff_policy
699 .expect_on_failure()
700 .return_const(Duration::from_secs(0));
701 backoff_policy
702 }
703
704 pub(crate) fn get_mock_retry_throttler() -> MockRetryThrottler {
705 let mut throttler = MockRetryThrottler::new();
706 throttler.expect_on_retry_failure().return_const(());
707 throttler
708 .expect_throttle_retry_attempt()
709 .return_const(false);
710 throttler.expect_on_success().return_const(());
711 throttler
712 }
713
714 pub(crate) fn get_headers_from_cache(
715 headers: CacheableResource<HeaderMap>,
716 ) -> Result<HeaderMap> {
717 match headers {
718 CacheableResource::New { data, .. } => Ok(data),
719 CacheableResource::NotModified => Err(CredentialsError::from_msg(
720 false,
721 "Expecting headers to be present",
722 )),
723 }
724 }
725
726 pub(crate) fn get_token_from_headers(headers: CacheableResource<HeaderMap>) -> Option<String> {
727 match headers {
728 CacheableResource::New { data, .. } => data
729 .get(AUTHORIZATION)
730 .and_then(|token_value| token_value.to_str().ok())
731 .and_then(|s| s.split_whitespace().nth(1))
732 .map(|s| s.to_string()),
733 CacheableResource::NotModified => None,
734 }
735 }
736
737 pub(crate) fn get_token_type_from_headers(
738 headers: CacheableResource<HeaderMap>,
739 ) -> Option<String> {
740 match headers {
741 CacheableResource::New { data, .. } => data
742 .get(AUTHORIZATION)
743 .and_then(|token_value| token_value.to_str().ok())
744 .and_then(|s| s.split_whitespace().next())
745 .map(|s| s.to_string()),
746 CacheableResource::NotModified => None,
747 }
748 }
749
750 pub static RSA_PRIVATE_KEY: LazyLock<RsaPrivateKey> = LazyLock::new(|| {
751 let p_str: &str = "141367881524527794394893355677826002829869068195396267579403819572502936761383874443619453704612633353803671595972343528718438130450055151198231345212263093247511629886734453413988207866331439612464122904648042654465604881130663408340669956544709445155137282157402427763452856646879397237752891502149781819597";
752 let q_str: &str = "179395413952110013801471600075409598322058038890563483332288896635704255883613060744402506322679437982046475766067250097809676406576067239936945362857700460740092421061356861438909617220234758121022105150630083703531219941303688818533566528599328339894969707615478438750812672509434761181735933851075292740309";
753 let e_str: &str = "65537";
754
755 let p = BigUint::parse_bytes(p_str.as_bytes(), 10).expect("Failed to parse prime P");
756 let q = BigUint::parse_bytes(q_str.as_bytes(), 10).expect("Failed to parse prime Q");
757 let public_exponent =
758 BigUint::parse_bytes(e_str.as_bytes(), 10).expect("Failed to parse public exponent");
759
760 RsaPrivateKey::from_primes(vec![p, q], public_exponent)
761 .expect("Failed to create RsaPrivateKey from primes")
762 });
763
764 pub static PKCS8_PK: LazyLock<String> = LazyLock::new(|| {
765 RSA_PRIVATE_KEY
766 .to_pkcs8_pem(LineEnding::LF)
767 .expect("Failed to encode key to PKCS#8 PEM")
768 .to_string()
769 });
770
771 pub fn b64_decode_to_json(s: String) -> serde_json::Value {
772 let decoded = String::from_utf8(
773 base64::engine::general_purpose::URL_SAFE_NO_PAD
774 .decode(s)
775 .unwrap(),
776 )
777 .unwrap();
778 serde_json::from_str(&decoded).unwrap()
779 }
780
781 #[cfg(target_os = "windows")]
782 #[test]
783 #[serial_test::serial]
784 fn adc_well_known_path_windows() {
785 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
786 let _appdata = ScopedEnv::set("APPDATA", "C:/Users/foo");
787 assert_eq!(
788 adc_well_known_path(),
789 Some("C:/Users/foo/gcloud/application_default_credentials.json".to_string())
790 );
791 assert_eq!(
792 adc_path(),
793 Some(AdcPath::WellKnown(
794 "C:/Users/foo/gcloud/application_default_credentials.json".to_string()
795 ))
796 );
797 }
798
799 #[cfg(target_os = "windows")]
800 #[test]
801 #[serial_test::serial]
802 fn adc_well_known_path_windows_no_appdata() {
803 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
804 let _appdata = ScopedEnv::remove("APPDATA");
805 assert_eq!(adc_well_known_path(), None);
806 assert_eq!(adc_path(), None);
807 }
808
809 #[cfg(not(target_os = "windows"))]
810 #[test]
811 #[serial_test::serial]
812 fn adc_well_known_path_posix() {
813 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
814 let _home = ScopedEnv::set("HOME", "/home/foo");
815 assert_eq!(
816 adc_well_known_path(),
817 Some("/home/foo/.config/gcloud/application_default_credentials.json".to_string())
818 );
819 assert_eq!(
820 adc_path(),
821 Some(AdcPath::WellKnown(
822 "/home/foo/.config/gcloud/application_default_credentials.json".to_string()
823 ))
824 );
825 }
826
827 #[cfg(not(target_os = "windows"))]
828 #[test]
829 #[serial_test::serial]
830 fn adc_well_known_path_posix_no_home() {
831 let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
832 let _appdata = ScopedEnv::remove("HOME");
833 assert_eq!(adc_well_known_path(), None);
834 assert_eq!(adc_path(), None);
835 }
836
837 #[test]
838 #[serial_test::serial]
839 fn adc_path_from_env() {
840 let _creds = ScopedEnv::set(
841 "GOOGLE_APPLICATION_CREDENTIALS",
842 "/usr/bar/application_default_credentials.json",
843 );
844 assert_eq!(
845 adc_path(),
846 Some(AdcPath::FromEnv(
847 "/usr/bar/application_default_credentials.json".to_string()
848 ))
849 );
850 }
851
852 #[test]
853 #[serial_test::serial]
854 fn load_adc_no_well_known_path_fallback_to_mds() {
855 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
856 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
859 }
860
861 #[test]
862 #[serial_test::serial]
863 fn load_adc_no_file_at_well_known_path_fallback_to_mds() {
864 let dir = tempfile::TempDir::new().unwrap();
866 let path = dir.path().to_str().unwrap();
867 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
868 let _e2 = ScopedEnv::set("HOME", path); let _e3 = ScopedEnv::set("APPDATA", path); assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
871 }
872
873 #[test]
874 #[serial_test::serial]
875 fn load_adc_no_file_at_env_is_error() {
876 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", "file-does-not-exist.json");
877 let err = load_adc().unwrap_err();
878 assert!(err.is_loading(), "{err:?}");
879 let msg = format!("{err:?}");
880 assert!(msg.contains("file-does-not-exist.json"), "{err:?}");
881 assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"), "{err:?}");
882 }
883
884 #[test]
885 #[serial_test::serial]
886 fn load_adc_success() {
887 let file = tempfile::NamedTempFile::new().unwrap();
888 let path = file.into_temp_path();
889 std::fs::write(&path, "contents").expect("Unable to write to temporary file.");
890 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
891
892 assert_eq!(
893 load_adc().unwrap(),
894 AdcContents::Contents("contents".to_string())
895 );
896 }
897
898 #[test_case(true; "retryable")]
899 #[test_case(false; "non-retryable")]
900 #[tokio::test]
901 async fn error_credentials(retryable: bool) {
902 let credentials = super::testing::error_credentials(retryable);
903 assert!(
904 credentials.universe_domain().await.is_none(),
905 "{credentials:?}"
906 );
907 let err = credentials.headers(Extensions::new()).await.err().unwrap();
908 assert_eq!(err.is_transient(), retryable, "{err:?}");
909 let err = credentials.headers(Extensions::new()).await.err().unwrap();
910 assert_eq!(err.is_transient(), retryable, "{err:?}");
911 }
912
913 #[tokio::test]
914 #[serial_test::serial]
915 async fn create_access_token_credentials_fallback_to_mds_with_quota_project_override() {
916 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
917 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); let _e4 = ScopedEnv::set(GOOGLE_CLOUD_QUOTA_PROJECT_VAR, "env-quota-project");
920
921 let mds = Builder::default()
922 .with_quota_project_id("test-quota-project")
923 .build()
924 .unwrap();
925 let fmt = format!("{mds:?}");
926 assert!(fmt.contains("MDSCredentials"));
927 assert!(
928 fmt.contains("env-quota-project"),
929 "Expected 'env-quota-project', got: {fmt}"
930 );
931 }
932
933 #[tokio::test]
934 #[serial_test::serial]
935 async fn create_access_token_credentials_with_quota_project_from_builder() {
936 let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
937 let _e2 = ScopedEnv::remove("HOME"); let _e3 = ScopedEnv::remove("APPDATA"); let _e4 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
940
941 let creds = Builder::default()
942 .with_quota_project_id("test-quota-project")
943 .build()
944 .unwrap();
945 let fmt = format!("{creds:?}");
946 assert!(
947 fmt.contains("test-quota-project"),
948 "Expected 'test-quota-project', got: {fmt}"
949 );
950 }
951
952 #[tokio::test]
953 #[serial_test::serial]
954 async fn create_access_token_service_account_credentials_with_scopes() -> TestResult {
955 let _e1 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
956 let mut service_account_key = serde_json::json!({
957 "type": "service_account",
958 "project_id": "test-project-id",
959 "private_key_id": "test-private-key-id",
960 "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
961 "client_email": "test-client-email",
962 "universe_domain": "test-universe-domain"
963 });
964
965 let scopes =
966 ["https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate"];
967
968 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
969
970 let file = tempfile::NamedTempFile::new().unwrap();
971 let path = file.into_temp_path();
972 std::fs::write(&path, service_account_key.to_string())
973 .expect("Unable to write to temporary file.");
974 let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
975
976 let sac = Builder::default()
977 .with_quota_project_id("test-quota-project")
978 .with_scopes(scopes)
979 .build()
980 .unwrap();
981
982 let headers = sac.headers(Extensions::new()).await?;
983 let token = get_token_from_headers(headers).unwrap();
984 let parts: Vec<_> = token.split('.').collect();
985 assert_eq!(parts.len(), 3);
986 let claims = b64_decode_to_json(parts.get(1).unwrap().to_string());
987
988 let fmt = format!("{sac:?}");
989 assert!(fmt.contains("ServiceAccountCredentials"));
990 assert!(fmt.contains("test-quota-project"));
991 assert_eq!(claims["scope"], scopes.join(" "));
992
993 Ok(())
994 }
995}