1mod 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, CredentialsError};
80use crate::headers_util::build_cacheable_headers;
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_pemfile::Item;
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 #[cfg(google_cloud_unstable_signed_url)]
340 pub fn build_signer(self) -> BuildResult<crate::signer::Signer> {
341 let service_account_key =
342 serde_json::from_value::<ServiceAccountKey>(self.service_account_key.clone())
343 .map_err(BuilderError::parsing)?;
344 let signing_provider =
345 crate::signer::service_account::ServiceAccountSigner::new(service_account_key);
346 Ok(crate::signer::Signer {
347 inner: Arc::new(signing_provider),
348 })
349 }
350}
351
352#[derive(serde::Deserialize, Default, Clone)]
356pub(crate) struct ServiceAccountKey {
357 pub(crate) client_email: String,
360 private_key_id: String,
362 private_key: String,
365 project_id: String,
367 universe_domain: Option<String>,
369}
370
371impl ServiceAccountKey {
372 pub(crate) fn signer(&self) -> Result<Box<dyn Signer>> {
374 let private_key = self.private_key.clone();
375 let key_provider = CryptoProvider::get_default().map_or_else(
376 || rustls::crypto::ring::default_provider().key_provider,
377 |p| p.key_provider,
378 );
379
380 let private_key = rustls_pemfile::read_one(&mut private_key.as_bytes())
381 .map_err(errors::non_retryable)?
382 .ok_or_else(|| {
383 errors::non_retryable_from_str("missing PEM section in service account key")
384 })?;
385 let pk = match private_key {
386 Item::Pkcs8Key(item) => key_provider.load_private_key(item.into()),
387 other => {
388 return Err(Self::unexpected_private_key_error(other));
389 }
390 };
391 let sk = pk.map_err(errors::non_retryable)?;
392 sk.choose_scheme(&[rustls::SignatureScheme::RSA_PKCS1_SHA256])
393 .ok_or_else(|| errors::non_retryable_from_str("Unable to choose RSA_PKCS1_SHA256 signing scheme as it is not supported by current signer"))
394 }
395
396 fn unexpected_private_key_error(private_key_format: Item) -> CredentialsError {
397 errors::non_retryable_from_str(format!(
398 "expected key to be in form of PKCS8, found {private_key_format:?}",
399 ))
400 }
401}
402
403impl std::fmt::Debug for ServiceAccountKey {
404 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405 f.debug_struct("ServiceAccountKey")
406 .field("client_email", &self.client_email)
407 .field("private_key_id", &self.private_key_id)
408 .field("private_key", &"[censored]")
409 .field("project_id", &self.project_id)
410 .field("universe_domain", &self.universe_domain)
411 .finish()
412 }
413}
414
415#[derive(Debug)]
416struct ServiceAccountCredentials<T>
417where
418 T: CachedTokenProvider,
419{
420 token_provider: T,
421 quota_project_id: Option<String>,
422}
423
424#[derive(Debug)]
425struct ServiceAccountTokenProvider {
426 service_account_key: ServiceAccountKey,
427 access_specifier: AccessSpecifier,
428}
429
430fn token_issue_time(current_time: OffsetDateTime) -> OffsetDateTime {
431 current_time - CLOCK_SKEW_FUDGE
432}
433
434fn token_expiry_time(current_time: OffsetDateTime) -> OffsetDateTime {
435 current_time + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT
436}
437
438#[async_trait]
439impl TokenProvider for ServiceAccountTokenProvider {
440 async fn token(&self) -> Result<Token> {
441 let expires_at = Instant::now() + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT;
442 let tg = ServiceAccountTokenGenerator {
443 audience: self.access_specifier.audience().cloned(),
444 scopes: self
445 .access_specifier
446 .scopes()
447 .map(|scopes| scopes.join(" ")),
448 service_account_key: self.service_account_key.clone(),
449 target_audience: None,
450 };
451
452 let token = tg.generate()?;
453
454 let token = Token {
455 token,
456 token_type: "Bearer".to_string(),
457 expires_at: Some(expires_at),
458 metadata: None,
459 };
460 Ok(token)
461 }
462}
463
464#[derive(Default, Clone)]
465pub(crate) struct ServiceAccountTokenGenerator {
466 service_account_key: ServiceAccountKey,
467 audience: Option<String>,
468 scopes: Option<String>,
469 target_audience: Option<String>,
470}
471
472impl ServiceAccountTokenGenerator {
473 #[cfg(feature = "idtoken")]
474 pub(crate) fn new_id_token_generator(
475 target_audience: String,
476 audience: String,
477 service_account_key: ServiceAccountKey,
478 ) -> Self {
479 Self {
480 service_account_key,
481 target_audience: Some(target_audience),
482 audience: Some(audience),
483 scopes: None,
484 }
485 }
486
487 pub(crate) fn generate(&self) -> Result<String> {
488 let signer = self.service_account_key.signer()?;
489
490 let current_time = OffsetDateTime::now_utc();
494
495 let claims = JwsClaims {
496 iss: self.service_account_key.client_email.clone(),
497 scope: self.scopes.clone(),
498 target_audience: self.target_audience.clone(),
499 aud: self.audience.clone(),
500 exp: token_expiry_time(current_time),
501 iat: token_issue_time(current_time),
502 typ: None,
503 sub: Some(self.service_account_key.client_email.clone()),
504 };
505
506 let header = JwsHeader {
507 alg: "RS256",
508 typ: "JWT",
509 kid: &self.service_account_key.private_key_id,
510 };
511 let encoded_header_claims = format!("{}.{}", header.encode()?, claims.encode()?);
512 let sig = signer
513 .sign(encoded_header_claims.as_bytes())
514 .map_err(errors::non_retryable)?;
515 use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine as _};
516 let token = format!(
517 "{}.{}",
518 encoded_header_claims,
519 &BASE64_URL_SAFE_NO_PAD.encode(sig)
520 );
521
522 Ok(token)
523 }
524}
525
526#[async_trait::async_trait]
527impl<T> CredentialsProvider for ServiceAccountCredentials<T>
528where
529 T: CachedTokenProvider,
530{
531 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
532 let token = self.token_provider.token(extensions).await?;
533 build_cacheable_headers(&token, &self.quota_project_id)
534 }
535}
536
537#[async_trait::async_trait]
538impl<T> AccessTokenCredentialsProvider for ServiceAccountCredentials<T>
539where
540 T: CachedTokenProvider,
541{
542 async fn access_token(&self) -> Result<AccessToken> {
543 let token = self.token_provider.token(Extensions::new()).await?;
544 token.into()
545 }
546}
547
548#[cfg(test)]
549mod tests {
550 use super::*;
551 use crate::credentials::QUOTA_PROJECT_KEY;
552 use crate::credentials::tests::{
553 PKCS8_PK, b64_decode_to_json, get_headers_from_cache, get_token_from_headers,
554 };
555 use crate::token::tests::MockTokenProvider;
556 use http::HeaderValue;
557 use http::header::AUTHORIZATION;
558 use rsa::pkcs1::EncodeRsaPrivateKey;
559 use rsa::pkcs8::LineEnding;
560 use rustls_pemfile::Item;
561 use serde_json::Value;
562 use serde_json::json;
563 use std::error::Error as _;
564 use std::time::Duration;
565
566 type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
567
568 const SSJ_REGEX: &str = r"(?<header>[^\.]+)\.(?<claims>[^\.]+)\.(?<sig>[^\.]+)";
569
570 #[test]
571 fn debug_token_provider() {
572 let expected = ServiceAccountKey {
573 client_email: "test-client-email".to_string(),
574 private_key_id: "test-private-key-id".to_string(),
575 private_key: "super-duper-secret-private-key".to_string(),
576 project_id: "test-project-id".to_string(),
577 universe_domain: Some("test-universe-domain".to_string()),
578 };
579 let fmt = format!("{expected:?}");
580 assert!(fmt.contains("test-client-email"), "{fmt}");
581 assert!(fmt.contains("test-private-key-id"), "{fmt}");
582 assert!(!fmt.contains("super-duper-secret-private-key"), "{fmt}");
583 assert!(fmt.contains("test-project-id"), "{fmt}");
584 assert!(fmt.contains("test-universe-domain"), "{fmt}");
585 }
586
587 #[test]
588 fn validate_token_issue_time() {
589 let current_time = OffsetDateTime::now_utc();
590 let token_issue_time = token_issue_time(current_time);
591 assert!(token_issue_time == current_time - CLOCK_SKEW_FUDGE);
592 }
593
594 #[test]
595 fn validate_token_expiry_time() {
596 let current_time = OffsetDateTime::now_utc();
597 let token_issue_time = token_expiry_time(current_time);
598 assert!(token_issue_time == current_time + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT);
599 }
600
601 #[tokio::test]
602 async fn headers_success_without_quota_project() -> TestResult {
603 let token = Token {
604 token: "test-token".to_string(),
605 token_type: "Bearer".to_string(),
606 expires_at: None,
607 metadata: None,
608 };
609
610 let mut mock = MockTokenProvider::new();
611 mock.expect_token().times(1).return_once(|| Ok(token));
612
613 let sac = ServiceAccountCredentials {
614 token_provider: TokenCache::new(mock),
615 quota_project_id: None,
616 };
617
618 let mut extensions = Extensions::new();
619 let cached_headers = sac.headers(extensions.clone()).await.unwrap();
620 let (headers, entity_tag) = match cached_headers {
621 CacheableResource::New { entity_tag, data } => (data, entity_tag),
622 CacheableResource::NotModified => unreachable!("expecting new headers"),
623 };
624 let token = headers.get(AUTHORIZATION).unwrap();
625
626 assert_eq!(headers.len(), 1, "{headers:?}");
627 assert_eq!(token, HeaderValue::from_static("Bearer test-token"));
628 assert!(token.is_sensitive());
629
630 extensions.insert(entity_tag);
631
632 let cached_headers = sac.headers(extensions).await?;
633
634 match cached_headers {
635 CacheableResource::New { .. } => unreachable!("expecting new headers"),
636 CacheableResource::NotModified => CacheableResource::<HeaderMap>::NotModified,
637 };
638 Ok(())
639 }
640
641 #[tokio::test]
642 async fn headers_success_with_quota_project() -> TestResult {
643 let token = Token {
644 token: "test-token".to_string(),
645 token_type: "Bearer".to_string(),
646 expires_at: None,
647 metadata: None,
648 };
649
650 let quota_project = "test-quota-project";
651
652 let mut mock = MockTokenProvider::new();
653 mock.expect_token().times(1).return_once(|| Ok(token));
654
655 let sac = ServiceAccountCredentials {
656 token_provider: TokenCache::new(mock),
657 quota_project_id: Some(quota_project.to_string()),
658 };
659
660 let headers = get_headers_from_cache(sac.headers(Extensions::new()).await.unwrap())?;
661 let token = headers.get(AUTHORIZATION).unwrap();
662 let quota_project_header = headers.get(QUOTA_PROJECT_KEY).unwrap();
663
664 assert_eq!(headers.len(), 2, "{headers:?}");
665 assert_eq!(token, HeaderValue::from_static("Bearer test-token"));
666 assert!(token.is_sensitive());
667 assert_eq!(
668 quota_project_header,
669 HeaderValue::from_static(quota_project)
670 );
671 assert!(!quota_project_header.is_sensitive());
672 Ok(())
673 }
674
675 #[tokio::test]
676 async fn headers_failure() {
677 let mut mock = MockTokenProvider::new();
678 mock.expect_token()
679 .times(1)
680 .return_once(|| Err(errors::non_retryable_from_str("fail")));
681
682 let sac = ServiceAccountCredentials {
683 token_provider: TokenCache::new(mock),
684 quota_project_id: None,
685 };
686 assert!(sac.headers(Extensions::new()).await.is_err());
687 }
688
689 fn get_mock_service_key() -> Value {
690 json!({
691 "client_email": "test-client-email",
692 "private_key_id": "test-private-key-id",
693 "private_key": "",
694 "project_id": "test-project-id",
695 })
696 }
697
698 #[tokio::test]
699 async fn get_service_account_headers_pkcs1_private_key_failure() -> TestResult {
700 let mut service_account_key = get_mock_service_key();
701
702 let key = crate::credentials::tests::RSA_PRIVATE_KEY
703 .to_pkcs1_pem(LineEnding::LF)
704 .expect("Failed to encode key to PKCS#1 PEM")
705 .to_string();
706
707 service_account_key["private_key"] = Value::from(key);
708 let cred = Builder::new(service_account_key).build()?;
709 let expected_error_message = "expected key to be in form of PKCS8, found Pkcs1Key";
710 assert!(
711 cred.headers(Extensions::new())
712 .await
713 .is_err_and(|e| e.to_string().contains(expected_error_message))
714 );
715 Ok(())
716 }
717
718 #[tokio::test]
719 async fn get_service_account_token_pkcs8_key_success() -> TestResult {
720 let mut service_account_key = get_mock_service_key();
721 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
722 let tp = Builder::new(service_account_key.clone()).build_token_provider()?;
723
724 let token = tp.token().await?;
725 let re = regex::Regex::new(SSJ_REGEX).unwrap();
726 let captures = re.captures(&token.token).ok_or_else(|| {
727 format!(
728 r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {}"#,
729 token.token
730 )
731 })?;
732 let header = b64_decode_to_json(captures["header"].to_string());
733 assert_eq!(header["alg"], "RS256");
734 assert_eq!(header["typ"], "JWT");
735 assert_eq!(header["kid"], service_account_key["private_key_id"]);
736
737 let claims = b64_decode_to_json(captures["claims"].to_string());
738 assert_eq!(claims["iss"], service_account_key["client_email"]);
739 assert_eq!(claims["scope"], DEFAULT_SCOPE);
740 assert!(claims["iat"].is_number());
741 assert!(claims["exp"].is_number());
742 assert_eq!(claims["sub"], service_account_key["client_email"]);
743
744 Ok(())
745 }
746
747 #[tokio::test]
748 async fn header_caching() -> TestResult {
749 let private_key = PKCS8_PK.clone();
750
751 let json_value = json!({
752 "client_email": "test-client-email",
753 "private_key_id": "test-private-key-id",
754 "private_key": private_key,
755 "project_id": "test-project-id",
756 "universe_domain": "test-universe-domain"
757 });
758
759 let credentials = Builder::new(json_value).build()?;
760
761 let headers = credentials.headers(Extensions::new()).await?;
762
763 let re = regex::Regex::new(SSJ_REGEX).unwrap();
764 let token = get_token_from_headers(headers).unwrap();
765
766 let captures = re.captures(&token).unwrap();
767
768 let claims = b64_decode_to_json(captures["claims"].to_string());
769 let first_iat = claims["iat"].as_i64().unwrap();
770
771 std::thread::sleep(Duration::from_secs(1));
776
777 let token = get_token_from_headers(credentials.headers(Extensions::new()).await?).unwrap();
779 let captures = re.captures(&token).unwrap();
780
781 let claims = b64_decode_to_json(captures["claims"].to_string());
782 let second_iat = claims["iat"].as_i64().unwrap();
783
784 assert_eq!(first_iat, second_iat);
787
788 Ok(())
789 }
790
791 #[tokio::test]
792 async fn get_service_account_headers_invalid_key_failure() -> TestResult {
793 let mut service_account_key = get_mock_service_key();
794 let pem_data = "-----BEGIN PRIVATE KEY-----\nMIGkAg==\n-----END PRIVATE KEY-----";
795 service_account_key["private_key"] = Value::from(pem_data);
796 let cred = Builder::new(service_account_key).build()?;
797
798 let token = cred.headers(Extensions::new()).await;
799 let err = token.unwrap_err();
800 assert!(!err.is_transient(), "{err:?}");
801 let source = err.source().and_then(|e| e.downcast_ref::<rustls::Error>());
802 assert!(matches!(source, Some(rustls::Error::General(_))), "{err:?}");
803 Ok(())
804 }
805
806 #[tokio::test]
807 async fn get_service_account_invalid_json_failure() -> TestResult {
808 let service_account_key = Value::from(" ");
809 let e = Builder::new(service_account_key).build().unwrap_err();
810 assert!(e.is_parsing(), "{e:?}");
811
812 Ok(())
813 }
814
815 #[test]
816 fn signer_failure() -> TestResult {
817 let tp = Builder::new(get_mock_service_key()).build_token_provider()?;
818 let tg = ServiceAccountTokenGenerator {
819 service_account_key: tp.service_account_key.clone(),
820 ..Default::default()
821 };
822
823 let signer = tg.service_account_key.signer();
824 let expected_error_message = "missing PEM section in service account key";
825 assert!(signer.is_err_and(|e| e.to_string().contains(expected_error_message)));
826 Ok(())
827 }
828
829 #[test]
830 fn unexpected_private_key_error_message() -> TestResult {
831 let expected_message = format!(
832 "expected key to be in form of PKCS8, found {:?}",
833 Item::Crl(Vec::new().into()) );
835
836 let error = ServiceAccountKey::unexpected_private_key_error(Item::Crl(Vec::new().into()));
837 assert!(error.to_string().contains(&expected_message));
838 Ok(())
839 }
840
841 #[tokio::test]
842 async fn get_service_account_headers_with_audience() -> TestResult {
843 let mut service_account_key = get_mock_service_key();
844 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
845 let headers = Builder::new(service_account_key.clone())
846 .with_access_specifier(AccessSpecifier::from_audience("test-audience"))
847 .build()?
848 .headers(Extensions::new())
849 .await?;
850
851 let re = regex::Regex::new(SSJ_REGEX).unwrap();
852 let token = get_token_from_headers(headers).unwrap();
853 let captures = re.captures(&token).ok_or_else(|| {
854 format!(r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {token}"#)
855 })?;
856 let token_header = b64_decode_to_json(captures["header"].to_string());
857 assert_eq!(token_header["alg"], "RS256");
858 assert_eq!(token_header["typ"], "JWT");
859 assert_eq!(token_header["kid"], service_account_key["private_key_id"]);
860
861 let claims = b64_decode_to_json(captures["claims"].to_string());
862 assert_eq!(claims["iss"], service_account_key["client_email"]);
863 assert_eq!(claims["scope"], Value::Null);
864 assert_eq!(claims["aud"], "test-audience");
865 assert!(claims["iat"].is_number());
866 assert!(claims["exp"].is_number());
867 assert_eq!(claims["sub"], service_account_key["client_email"]);
868 Ok(())
869 }
870
871 #[tokio::test(start_paused = true)]
872 async fn get_service_account_token_verify_expiry_time() -> TestResult {
873 let now = Instant::now();
874 let mut service_account_key = get_mock_service_key();
875 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
876 let token = Builder::new(service_account_key)
877 .build_token_provider()?
878 .token()
879 .await?;
880
881 let expected_expiry = now + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT;
882
883 assert_eq!(token.expires_at.unwrap(), expected_expiry);
884 Ok(())
885 }
886
887 #[tokio::test]
888 async fn get_service_account_headers_with_custom_scopes() -> TestResult {
889 let mut service_account_key = get_mock_service_key();
890 let scopes = vec![
891 "https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate",
892 ];
893 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
894 let headers = Builder::new(service_account_key.clone())
895 .with_access_specifier(AccessSpecifier::from_scopes(scopes.clone()))
896 .build()?
897 .headers(Extensions::new())
898 .await?;
899
900 let re = regex::Regex::new(SSJ_REGEX).unwrap();
901 let token = get_token_from_headers(headers).unwrap();
902 let captures = re.captures(&token).ok_or_else(|| {
903 format!(r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {token}"#)
904 })?;
905 let token_header = b64_decode_to_json(captures["header"].to_string());
906 assert_eq!(token_header["alg"], "RS256");
907 assert_eq!(token_header["typ"], "JWT");
908 assert_eq!(token_header["kid"], service_account_key["private_key_id"]);
909
910 let claims = b64_decode_to_json(captures["claims"].to_string());
911 assert_eq!(claims["iss"], service_account_key["client_email"]);
912 assert_eq!(claims["scope"], scopes.join(" "));
913 assert_eq!(claims["aud"], Value::Null);
914 assert!(claims["iat"].is_number());
915 assert!(claims["exp"].is_number());
916 assert_eq!(claims["sub"], service_account_key["client_email"]);
917 Ok(())
918 }
919
920 #[tokio::test]
921 async fn get_service_account_access_token() -> TestResult {
922 let mut service_account_key = get_mock_service_key();
923 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
924 let creds = Builder::new(service_account_key.clone()).build_access_token_credentials()?;
925
926 let access_token = creds.access_token().await?;
927 let token = access_token.token;
928
929 let re = regex::Regex::new(SSJ_REGEX).unwrap();
930 let captures = re.captures(&token).ok_or_else(|| {
931 format!(r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {token}"#)
932 })?;
933 let token_header = b64_decode_to_json(captures["header"].to_string());
934 assert_eq!(token_header["alg"], "RS256");
935 assert_eq!(token_header["typ"], "JWT");
936 assert_eq!(token_header["kid"], service_account_key["private_key_id"]);
937
938 Ok(())
939 }
940
941 #[tokio::test]
942 #[cfg(google_cloud_unstable_signed_url)]
943 async fn get_service_account_signer() -> TestResult {
944 let mut service_account_key = get_mock_service_key();
945 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
946 let signer = Builder::new(service_account_key.clone()).build_signer()?;
947
948 let client_email = signer.client_email().await?;
949 assert_eq!(client_email, service_account_key["client_email"]);
950
951 let result = signer.sign(b"test").await;
952
953 assert!(result.is_ok());
954
955 Ok(())
956 }
957}