1pub(crate) mod jws;
74
75use crate::build_errors::Error as BuilderError;
76use crate::constants::DEFAULT_SCOPE;
77use crate::credentials::dynamic::{AccessTokenCredentialsProvider, CredentialsProvider};
78use crate::credentials::{AccessToken, AccessTokenCredentials, CacheableResource, Credentials};
79use crate::errors::{self};
80use crate::headers_util::AuthHeadersBuilder;
81use crate::token::{CachedTokenProvider, Token, TokenProvider};
82use crate::token_cache::TokenCache;
83use crate::{BuildResult, Result};
84use async_trait::async_trait;
85use http::{Extensions, HeaderMap};
86use jws::{CLOCK_SKEW_FUDGE, DEFAULT_TOKEN_TIMEOUT, JwsClaims, JwsHeader};
87use rustls::crypto::CryptoProvider;
88use rustls::sign::Signer;
89use rustls_pki_types::{PrivateKeyDer, pem::PemObject};
90use serde_json::Value;
91use std::sync::Arc;
92use time::OffsetDateTime;
93use tokio::time::Instant;
94
95#[derive(Clone, Debug, PartialEq)]
104pub enum AccessSpecifier {
105 Audience(String),
112
113 Scopes(Vec<String>),
132}
133
134impl AccessSpecifier {
135 fn audience(&self) -> Option<&String> {
136 match self {
137 AccessSpecifier::Audience(aud) => Some(aud),
138 AccessSpecifier::Scopes(_) => None,
139 }
140 }
141
142 fn scopes(&self) -> Option<&[String]> {
143 match self {
144 AccessSpecifier::Scopes(scopes) => Some(scopes),
145 AccessSpecifier::Audience(_) => None,
146 }
147 }
148
149 pub fn from_scopes<I, S>(scopes: I) -> Self
163 where
164 I: IntoIterator<Item = S>,
165 S: Into<String>,
166 {
167 AccessSpecifier::Scopes(scopes.into_iter().map(|s| s.into()).collect())
168 }
169
170 pub fn from_audience<S: Into<String>>(audience: S) -> Self {
184 AccessSpecifier::Audience(audience.into())
185 }
186}
187
188pub struct Builder {
207 service_account_key: Value,
208 access_specifier: AccessSpecifier,
209 quota_project_id: Option<String>,
210}
211
212impl Builder {
213 pub fn new(service_account_key: Value) -> Self {
220 Self {
221 service_account_key,
222 access_specifier: AccessSpecifier::Scopes([DEFAULT_SCOPE].map(str::to_string).to_vec()),
223 quota_project_id: None,
224 }
225 }
226
227 pub fn with_access_specifier(mut self, access_specifier: AccessSpecifier) -> Self {
249 self.access_specifier = access_specifier;
250 self
251 }
252
253 pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
262 self.quota_project_id = Some(quota_project_id.into());
263 self
264 }
265
266 fn build_token_provider(self) -> BuildResult<ServiceAccountTokenProvider> {
267 let service_account_key =
268 serde_json::from_value::<ServiceAccountKey>(self.service_account_key)
269 .map_err(BuilderError::parsing)?;
270
271 Ok(ServiceAccountTokenProvider {
272 service_account_key,
273 access_specifier: self.access_specifier,
274 })
275 }
276
277 pub fn build(self) -> BuildResult<Credentials> {
291 Ok(self.build_access_token_credentials()?.into())
292 }
293
294 pub fn build_access_token_credentials(self) -> BuildResult<AccessTokenCredentials> {
331 Ok(AccessTokenCredentials {
332 inner: Arc::new(ServiceAccountCredentials {
333 quota_project_id: self.quota_project_id.clone(),
334 token_provider: TokenCache::new(self.build_token_provider()?),
335 }),
336 })
337 }
338
339 pub fn build_signer(self) -> BuildResult<crate::signer::Signer> {
368 let service_account_key =
369 serde_json::from_value::<ServiceAccountKey>(self.service_account_key.clone())
370 .map_err(BuilderError::parsing)?;
371 let signing_provider =
372 crate::signer::service_account::ServiceAccountSigner::new(service_account_key);
373 Ok(crate::signer::Signer {
374 inner: Arc::new(signing_provider),
375 })
376 }
377}
378
379#[derive(serde::Deserialize, Default, Clone)]
383pub(crate) struct ServiceAccountKey {
384 pub(crate) client_email: String,
387 private_key_id: String,
389 private_key: String,
392 project_id: String,
394 universe_domain: Option<String>,
396}
397
398impl ServiceAccountKey {
399 pub(crate) fn signer(&self) -> Result<Box<dyn Signer>> {
401 let private_key = self.private_key.clone();
402 let key_provider = CryptoProvider::get_default().map(|p| p.key_provider);
403 #[cfg(feature = "default-rustls-provider")]
404 let key_provider = key_provider
405 .unwrap_or_else(|| rustls::crypto::aws_lc_rs::default_provider().key_provider);
406 #[cfg(not(feature = "default-rustls-provider"))]
407 let key_provider = key_provider.expect(
408 r###"
409The default rustls::CryptoProvider should be configured by the application. The
410`google-cloud-auth` crate was compiled without the `default-rustls-provider`
411feature. Without this feature the crate expects the application to initialize
412the rustls crypto provider using `rustls::CryptoProvider::install_default()`.
413
414Note that the application must use the exact same version of `rustls` as the
415`google-cloud-auth` crate does. Otherwise `install_default()` has no effect."###,
416 );
417
418 let key_der = PrivateKeyDer::from_pem_slice(private_key.as_bytes()).map_err(|e| {
419 errors::non_retryable_from_str(format!(
420 "Failed to parse service account private key PEM: {}",
421 e
422 ))
423 })?;
424
425 let pkcs8_der = match key_der {
426 PrivateKeyDer::Pkcs8(der) => der,
427 _ => {
428 return Err(errors::non_retryable_from_str(format!(
429 "expected key to be in form of PKCS8, found {:?}",
430 key_der
431 )));
432 }
433 };
434
435 let pk = key_provider
436 .load_private_key(PrivateKeyDer::Pkcs8(pkcs8_der))
437 .map_err(errors::non_retryable)?;
438
439 pk.choose_scheme(&[rustls::SignatureScheme::RSA_PKCS1_SHA256])
440 .ok_or_else(||{
441 errors::non_retryable_from_str("Unable to choose RSA_PKCS1_SHA256 signing scheme as it is not supported by current signer")
442 })
443 }
444}
445
446impl std::fmt::Debug for ServiceAccountKey {
447 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
448 f.debug_struct("ServiceAccountKey")
449 .field("client_email", &self.client_email)
450 .field("private_key_id", &self.private_key_id)
451 .field("private_key", &"[censored]")
452 .field("project_id", &self.project_id)
453 .field("universe_domain", &self.universe_domain)
454 .finish()
455 }
456}
457
458#[derive(Debug)]
459struct ServiceAccountCredentials<T>
460where
461 T: CachedTokenProvider,
462{
463 token_provider: T,
464 quota_project_id: Option<String>,
465}
466
467#[derive(Debug)]
468struct ServiceAccountTokenProvider {
469 service_account_key: ServiceAccountKey,
470 access_specifier: AccessSpecifier,
471}
472
473fn token_issue_time(current_time: OffsetDateTime) -> OffsetDateTime {
474 current_time - CLOCK_SKEW_FUDGE
475}
476
477fn token_expiry_time(current_time: OffsetDateTime) -> OffsetDateTime {
478 current_time + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT
479}
480
481#[async_trait]
482impl TokenProvider for ServiceAccountTokenProvider {
483 async fn token(&self) -> Result<Token> {
484 let expires_at = Instant::now() + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT;
485 let tg = ServiceAccountTokenGenerator {
486 audience: self.access_specifier.audience().cloned(),
487 scopes: self
488 .access_specifier
489 .scopes()
490 .map(|scopes| scopes.join(" ")),
491 service_account_key: self.service_account_key.clone(),
492 target_audience: None,
493 };
494
495 let token = tg.generate()?;
496
497 let token = Token {
498 token,
499 token_type: "Bearer".to_string(),
500 expires_at: Some(expires_at),
501 metadata: None,
502 };
503 Ok(token)
504 }
505}
506
507#[derive(Default, Clone)]
508pub(crate) struct ServiceAccountTokenGenerator {
509 service_account_key: ServiceAccountKey,
510 audience: Option<String>,
511 scopes: Option<String>,
512 target_audience: Option<String>,
513}
514
515impl ServiceAccountTokenGenerator {
516 #[cfg(feature = "idtoken")]
517 pub(crate) fn new_id_token_generator(
518 target_audience: String,
519 audience: String,
520 service_account_key: ServiceAccountKey,
521 ) -> Self {
522 Self {
523 service_account_key,
524 target_audience: Some(target_audience),
525 audience: Some(audience),
526 scopes: None,
527 }
528 }
529
530 pub(crate) fn generate(&self) -> Result<String> {
531 let signer = self.service_account_key.signer()?;
532
533 let current_time = OffsetDateTime::now_utc();
537
538 let claims = JwsClaims {
539 iss: self.service_account_key.client_email.clone(),
540 scope: self.scopes.clone(),
541 target_audience: self.target_audience.clone(),
542 aud: self.audience.clone(),
543 exp: token_expiry_time(current_time),
544 iat: token_issue_time(current_time),
545 typ: None,
546 sub: Some(self.service_account_key.client_email.clone()),
547 };
548
549 let header = JwsHeader {
550 alg: "RS256",
551 typ: "JWT",
552 kid: Some(self.service_account_key.private_key_id.clone()),
553 };
554 let encoded_header_claims = format!("{}.{}", header.encode()?, claims.encode()?);
555 let sig = signer
556 .sign(encoded_header_claims.as_bytes())
557 .map_err(errors::non_retryable)?;
558 use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine as _};
559 let token = format!(
560 "{}.{}",
561 encoded_header_claims,
562 &BASE64_URL_SAFE_NO_PAD.encode(sig)
563 );
564
565 Ok(token)
566 }
567}
568
569#[async_trait::async_trait]
570impl<T> CredentialsProvider for ServiceAccountCredentials<T>
571where
572 T: CachedTokenProvider,
573{
574 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
575 let token = self.token_provider.token(extensions).await?;
576
577 AuthHeadersBuilder::new(&token)
578 .maybe_quota_project_id(self.quota_project_id.as_deref())
579 .build()
580 }
581}
582
583#[async_trait::async_trait]
584impl<T> AccessTokenCredentialsProvider for ServiceAccountCredentials<T>
585where
586 T: CachedTokenProvider,
587{
588 async fn access_token(&self) -> Result<AccessToken> {
589 let token = self.token_provider.token(Extensions::new()).await?;
590 token.into()
591 }
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597 use crate::credentials::QUOTA_PROJECT_KEY;
598 use crate::credentials::tests::{
599 PKCS8_PK, b64_decode_to_json, get_headers_from_cache, get_token_from_headers,
600 };
601 use crate::token::tests::MockTokenProvider;
602 use http::HeaderValue;
603 use http::header::AUTHORIZATION;
604 use rsa::pkcs1::EncodeRsaPrivateKey;
605 use rsa::pkcs8::LineEnding;
606 use serde_json::Value;
607 use serde_json::json;
608 use std::error::Error as _;
609 use std::time::Duration;
610
611 type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
612
613 const SSJ_REGEX: &str = r"(?<header>[^\.]+)\.(?<claims>[^\.]+)\.(?<sig>[^\.]+)";
614
615 #[test]
616 fn debug_token_provider() {
617 let expected = ServiceAccountKey {
618 client_email: "test-client-email".to_string(),
619 private_key_id: "test-private-key-id".to_string(),
620 private_key: "super-duper-secret-private-key".to_string(),
621 project_id: "test-project-id".to_string(),
622 universe_domain: Some("test-universe-domain".to_string()),
623 };
624 let fmt = format!("{expected:?}");
625 assert!(fmt.contains("test-client-email"), "{fmt}");
626 assert!(fmt.contains("test-private-key-id"), "{fmt}");
627 assert!(!fmt.contains("super-duper-secret-private-key"), "{fmt}");
628 assert!(fmt.contains("test-project-id"), "{fmt}");
629 assert!(fmt.contains("test-universe-domain"), "{fmt}");
630 }
631
632 #[test]
633 fn validate_token_issue_time() {
634 let current_time = OffsetDateTime::now_utc();
635 let token_issue_time = token_issue_time(current_time);
636 assert!(token_issue_time == current_time - CLOCK_SKEW_FUDGE);
637 }
638
639 #[test]
640 fn validate_token_expiry_time() {
641 let current_time = OffsetDateTime::now_utc();
642 let token_issue_time = token_expiry_time(current_time);
643 assert!(token_issue_time == current_time + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT);
644 }
645
646 #[tokio::test]
647 async fn headers_success_without_quota_project() -> TestResult {
648 let token = Token {
649 token: "test-token".to_string(),
650 token_type: "Bearer".to_string(),
651 expires_at: None,
652 metadata: None,
653 };
654
655 let mut mock = MockTokenProvider::new();
656 mock.expect_token().times(1).return_once(|| Ok(token));
657
658 let sac = ServiceAccountCredentials {
659 token_provider: TokenCache::new(mock),
660 quota_project_id: None,
661 };
662
663 let mut extensions = Extensions::new();
664 let cached_headers = sac.headers(extensions.clone()).await.unwrap();
665 let (headers, entity_tag) = match cached_headers {
666 CacheableResource::New { entity_tag, data } => (data, entity_tag),
667 CacheableResource::NotModified => unreachable!("expecting new headers"),
668 };
669 let token = headers.get(AUTHORIZATION).unwrap();
670
671 assert_eq!(headers.len(), 1, "{headers:?}");
672 assert_eq!(token, HeaderValue::from_static("Bearer test-token"));
673 assert!(token.is_sensitive());
674
675 extensions.insert(entity_tag);
676
677 let cached_headers = sac.headers(extensions).await?;
678
679 match cached_headers {
680 CacheableResource::New { .. } => unreachable!("expecting new headers"),
681 CacheableResource::NotModified => CacheableResource::<HeaderMap>::NotModified,
682 };
683 Ok(())
684 }
685
686 #[tokio::test]
687 async fn headers_success_with_quota_project() -> TestResult {
688 let token = Token {
689 token: "test-token".to_string(),
690 token_type: "Bearer".to_string(),
691 expires_at: None,
692 metadata: None,
693 };
694
695 let quota_project = "test-quota-project";
696
697 let mut mock = MockTokenProvider::new();
698 mock.expect_token().times(1).return_once(|| Ok(token));
699
700 let sac = ServiceAccountCredentials {
701 token_provider: TokenCache::new(mock),
702 quota_project_id: Some(quota_project.to_string()),
703 };
704
705 let headers = get_headers_from_cache(sac.headers(Extensions::new()).await.unwrap())?;
706 let token = headers.get(AUTHORIZATION).unwrap();
707 let quota_project_header = headers.get(QUOTA_PROJECT_KEY).unwrap();
708
709 assert_eq!(headers.len(), 2, "{headers:?}");
710 assert_eq!(token, HeaderValue::from_static("Bearer test-token"));
711 assert!(token.is_sensitive());
712 assert_eq!(
713 quota_project_header,
714 HeaderValue::from_static(quota_project)
715 );
716 assert!(!quota_project_header.is_sensitive());
717 Ok(())
718 }
719
720 #[tokio::test]
721 async fn headers_failure() {
722 let mut mock = MockTokenProvider::new();
723 mock.expect_token()
724 .times(1)
725 .return_once(|| Err(errors::non_retryable_from_str("fail")));
726
727 let sac = ServiceAccountCredentials {
728 token_provider: TokenCache::new(mock),
729 quota_project_id: None,
730 };
731 let result = sac.headers(Extensions::new()).await;
732 assert!(result.is_err(), "{result:?}");
733 }
734
735 fn get_mock_service_key() -> Value {
736 json!({
737 "client_email": "test-client-email",
738 "private_key_id": "test-private-key-id",
739 "private_key": "",
740 "project_id": "test-project-id",
741 })
742 }
743
744 #[tokio::test]
745 async fn get_service_account_headers_pkcs1_private_key_failure() -> TestResult {
746 let mut service_account_key = get_mock_service_key();
747
748 let key = crate::credentials::tests::RSA_PRIVATE_KEY
749 .to_pkcs1_pem(LineEnding::LF)
750 .expect("Failed to encode key to PKCS#1 PEM")
751 .to_string();
752
753 service_account_key["private_key"] = Value::from(key);
754 let cred = Builder::new(service_account_key).build()?;
755 let expected_error_message = "expected key to be in form of PKCS8, found ";
756 assert!(
757 cred.headers(Extensions::new())
758 .await
759 .is_err_and(|e| e.to_string().contains(expected_error_message))
760 );
761 Ok(())
762 }
763
764 #[tokio::test]
765 async fn get_service_account_token_pkcs8_key_success() -> TestResult {
766 let mut service_account_key = get_mock_service_key();
767 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
768 let tp = Builder::new(service_account_key.clone()).build_token_provider()?;
769
770 let token = tp.token().await?;
771 let re = regex::Regex::new(SSJ_REGEX).unwrap();
772 let captures = re.captures(&token.token).ok_or_else(|| {
773 format!(
774 r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {}"#,
775 token.token
776 )
777 })?;
778 let header = b64_decode_to_json(captures["header"].to_string());
779 assert_eq!(header["alg"], "RS256");
780 assert_eq!(header["typ"], "JWT");
781 assert_eq!(header["kid"], service_account_key["private_key_id"]);
782
783 let claims = b64_decode_to_json(captures["claims"].to_string());
784 assert_eq!(claims["iss"], service_account_key["client_email"]);
785 assert_eq!(claims["scope"], DEFAULT_SCOPE);
786 assert!(claims["iat"].is_number());
787 assert!(claims["exp"].is_number());
788 assert_eq!(claims["sub"], service_account_key["client_email"]);
789
790 Ok(())
791 }
792
793 #[tokio::test]
794 async fn header_caching() -> TestResult {
795 let private_key = PKCS8_PK.clone();
796
797 let json_value = json!({
798 "client_email": "test-client-email",
799 "private_key_id": "test-private-key-id",
800 "private_key": private_key,
801 "project_id": "test-project-id",
802 "universe_domain": "test-universe-domain"
803 });
804
805 let credentials = Builder::new(json_value).build()?;
806
807 let headers = credentials.headers(Extensions::new()).await?;
808
809 let re = regex::Regex::new(SSJ_REGEX).unwrap();
810 let token = get_token_from_headers(headers).unwrap();
811
812 let captures = re.captures(&token).unwrap();
813
814 let claims = b64_decode_to_json(captures["claims"].to_string());
815 let first_iat = claims["iat"].as_i64().unwrap();
816
817 std::thread::sleep(Duration::from_secs(1));
822
823 let token = get_token_from_headers(credentials.headers(Extensions::new()).await?).unwrap();
825 let captures = re.captures(&token).unwrap();
826
827 let claims = b64_decode_to_json(captures["claims"].to_string());
828 let second_iat = claims["iat"].as_i64().unwrap();
829
830 assert_eq!(first_iat, second_iat);
833
834 Ok(())
835 }
836
837 #[tokio::test]
838 async fn get_service_account_headers_invalid_key_failure() -> TestResult {
839 let mut service_account_key = get_mock_service_key();
840 let pem_data = "-----BEGIN PRIVATE KEY-----\nMIGkAg==\n-----END PRIVATE KEY-----";
841 service_account_key["private_key"] = Value::from(pem_data);
842 let cred = Builder::new(service_account_key).build()?;
843
844 let token = cred.headers(Extensions::new()).await;
845 let err = token.unwrap_err();
846 assert!(!err.is_transient(), "{err:?}");
847 let source = err.source().and_then(|e| e.downcast_ref::<rustls::Error>());
848 assert!(matches!(source, Some(rustls::Error::General(_))), "{err:?}");
849 Ok(())
850 }
851
852 #[tokio::test]
853 async fn get_service_account_invalid_json_failure() -> TestResult {
854 let service_account_key = Value::from(" ");
855 let e = Builder::new(service_account_key).build().unwrap_err();
856 assert!(e.is_parsing(), "{e:?}");
857
858 Ok(())
859 }
860
861 #[test]
862 fn signer_failure() -> TestResult {
863 let tp = Builder::new(get_mock_service_key()).build_token_provider()?;
864 let tg = ServiceAccountTokenGenerator {
865 service_account_key: tp.service_account_key.clone(),
866 ..Default::default()
867 };
868
869 let signer = tg.service_account_key.signer();
870 let expected_error_message = "Failed to parse service account private key PEM";
871 assert!(signer.is_err_and(|e| e.to_string().contains(expected_error_message)));
872 Ok(())
873 }
874
875 #[test]
876 fn signer_fails_on_invalid_pem_type() -> TestResult {
877 let invalid_pem = concat!(
878 "-----BEGI X509 CRL-----\n",
879 "MIIBmzCBja... (truncated) ...\n",
880 "-----END X509 CRL-----"
881 );
882
883 let mut key = ServiceAccountKey {
884 private_key: invalid_pem.to_string(),
885 ..Default::default()
886 };
887 key.private_key = invalid_pem.to_string();
888 let result = key.signer();
889 assert!(result.is_err(), "{result:?}");
890 let error_msg = result.unwrap_err().to_string();
891 assert!(error_msg.contains("Failed to parse service account private key PEM"));
892 Ok(())
893 }
894
895 #[tokio::test]
896 async fn get_service_account_headers_with_audience() -> TestResult {
897 let mut service_account_key = get_mock_service_key();
898 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
899 let headers = Builder::new(service_account_key.clone())
900 .with_access_specifier(AccessSpecifier::from_audience("test-audience"))
901 .build()?
902 .headers(Extensions::new())
903 .await?;
904
905 let re = regex::Regex::new(SSJ_REGEX).unwrap();
906 let token = get_token_from_headers(headers).unwrap();
907 let captures = re.captures(&token).ok_or_else(|| {
908 format!(r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {token}"#)
909 })?;
910 let token_header = b64_decode_to_json(captures["header"].to_string());
911 assert_eq!(token_header["alg"], "RS256");
912 assert_eq!(token_header["typ"], "JWT");
913 assert_eq!(token_header["kid"], service_account_key["private_key_id"]);
914
915 let claims = b64_decode_to_json(captures["claims"].to_string());
916 assert_eq!(claims["iss"], service_account_key["client_email"]);
917 assert_eq!(claims["scope"], Value::Null);
918 assert_eq!(claims["aud"], "test-audience");
919 assert!(claims["iat"].is_number());
920 assert!(claims["exp"].is_number());
921 assert_eq!(claims["sub"], service_account_key["client_email"]);
922 Ok(())
923 }
924
925 #[tokio::test(start_paused = true)]
926 async fn get_service_account_token_verify_expiry_time() -> TestResult {
927 let now = Instant::now();
928 let mut service_account_key = get_mock_service_key();
929 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
930 let token = Builder::new(service_account_key)
931 .build_token_provider()?
932 .token()
933 .await?;
934
935 let expected_expiry = now + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT;
936
937 assert_eq!(token.expires_at.unwrap(), expected_expiry);
938 Ok(())
939 }
940
941 #[tokio::test]
942 async fn get_service_account_headers_with_custom_scopes() -> TestResult {
943 let mut service_account_key = get_mock_service_key();
944 let scopes = vec![
945 "https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate",
946 ];
947 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
948 let headers = Builder::new(service_account_key.clone())
949 .with_access_specifier(AccessSpecifier::from_scopes(scopes.clone()))
950 .build()?
951 .headers(Extensions::new())
952 .await?;
953
954 let re = regex::Regex::new(SSJ_REGEX).unwrap();
955 let token = get_token_from_headers(headers).unwrap();
956 let captures = re.captures(&token).ok_or_else(|| {
957 format!(r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {token}"#)
958 })?;
959 let token_header = b64_decode_to_json(captures["header"].to_string());
960 assert_eq!(token_header["alg"], "RS256");
961 assert_eq!(token_header["typ"], "JWT");
962 assert_eq!(token_header["kid"], service_account_key["private_key_id"]);
963
964 let claims = b64_decode_to_json(captures["claims"].to_string());
965 assert_eq!(claims["iss"], service_account_key["client_email"]);
966 assert_eq!(claims["scope"], scopes.join(" "));
967 assert_eq!(claims["aud"], Value::Null);
968 assert!(claims["iat"].is_number());
969 assert!(claims["exp"].is_number());
970 assert_eq!(claims["sub"], service_account_key["client_email"]);
971 Ok(())
972 }
973
974 #[tokio::test]
975 async fn get_service_account_access_token() -> TestResult {
976 let mut service_account_key = get_mock_service_key();
977 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
978 let creds = Builder::new(service_account_key.clone()).build_access_token_credentials()?;
979
980 let access_token = creds.access_token().await?;
981 let token = access_token.token;
982
983 let re = regex::Regex::new(SSJ_REGEX).unwrap();
984 let captures = re.captures(&token).ok_or_else(|| {
985 format!(r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {token}"#)
986 })?;
987 let token_header = b64_decode_to_json(captures["header"].to_string());
988 assert_eq!(token_header["alg"], "RS256");
989 assert_eq!(token_header["typ"], "JWT");
990 assert_eq!(token_header["kid"], service_account_key["private_key_id"]);
991
992 Ok(())
993 }
994
995 #[tokio::test]
996 async fn get_service_account_signer() -> TestResult {
997 let mut service_account_key = get_mock_service_key();
998 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
999 let signer = Builder::new(service_account_key.clone()).build_signer()?;
1000
1001 let client_email = signer.client_email().await?;
1002 assert_eq!(client_email, service_account_key["client_email"]);
1003
1004 let _bytes = signer.sign(b"test").await?;
1005
1006 Ok(())
1007 }
1008}