1pub(crate) mod jws;
73
74use crate::access_boundary::CredentialsWithAccessBoundary;
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::sign::Signer;
88use rustls_pki_types::{PrivateKeyDer, pem::PemObject};
89use serde_json::Value;
90use std::sync::Arc;
91use time::OffsetDateTime;
92use tokio::time::Instant;
93
94#[derive(Clone, Debug, PartialEq)]
103pub enum AccessSpecifier {
104 Audience(String),
111
112 Scopes(Vec<String>),
131}
132
133impl AccessSpecifier {
134 fn audience(&self) -> Option<&String> {
135 match self {
136 AccessSpecifier::Audience(aud) => Some(aud),
137 AccessSpecifier::Scopes(_) => None,
138 }
139 }
140
141 fn scopes(&self) -> Option<&[String]> {
142 match self {
143 AccessSpecifier::Scopes(scopes) => Some(scopes),
144 AccessSpecifier::Audience(_) => None,
145 }
146 }
147
148 pub fn from_scopes<I, S>(scopes: I) -> Self
162 where
163 I: IntoIterator<Item = S>,
164 S: Into<String>,
165 {
166 AccessSpecifier::Scopes(scopes.into_iter().map(|s| s.into()).collect())
167 }
168
169 pub fn from_audience<S: Into<String>>(audience: S) -> Self {
183 AccessSpecifier::Audience(audience.into())
184 }
185}
186
187pub struct Builder {
206 service_account_key: Value,
207 access_specifier: AccessSpecifier,
208 quota_project_id: Option<String>,
209 universe_domain: Option<String>,
210 iam_endpoint_override: Option<String>,
211}
212
213impl Builder {
214 pub fn new(service_account_key: Value) -> Self {
221 Self {
222 service_account_key,
223 access_specifier: AccessSpecifier::Scopes([DEFAULT_SCOPE].map(str::to_string).to_vec()),
224 quota_project_id: None,
225 universe_domain: None,
226 iam_endpoint_override: None,
227 }
228 }
229
230 pub fn with_access_specifier(mut self, access_specifier: AccessSpecifier) -> Self {
252 self.access_specifier = access_specifier;
253 self
254 }
255
256 pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
265 self.quota_project_id = Some(quota_project_id.into());
266 self
267 }
268
269 pub fn with_universe_domain<S: Into<String>>(mut self, universe_domain: S) -> Self {
292 self.universe_domain = Some(universe_domain.into());
293 self
294 }
295
296 #[cfg(all(test, google_cloud_unstable_trusted_boundaries))]
297 fn maybe_iam_endpoint_override(mut self, iam_endpoint_override: Option<String>) -> Self {
298 self.iam_endpoint_override = iam_endpoint_override;
299 self
300 }
301
302 fn build_token_provider(self) -> BuildResult<ServiceAccountTokenProvider> {
303 let service_account_key =
304 serde_json::from_value::<ServiceAccountKey>(self.service_account_key)
305 .map_err(BuilderError::parsing)?;
306
307 Ok(ServiceAccountTokenProvider {
308 service_account_key,
309 access_specifier: self.access_specifier,
310 })
311 }
312
313 pub fn build(self) -> BuildResult<Credentials> {
327 Ok(self.build_credentials()?.into())
328 }
329
330 pub fn build_access_token_credentials(self) -> BuildResult<AccessTokenCredentials> {
366 Ok(self.build_credentials()?.into())
367 }
368
369 fn build_credentials(
370 self,
371 ) -> BuildResult<CredentialsWithAccessBoundary<ServiceAccountCredentials<TokenCache>>> {
372 let iam_endpoint = self.iam_endpoint_override.clone();
373 let quota_project_id = self.quota_project_id.clone();
374 let universe_domain_override = self.universe_domain.clone();
375 let token_provider = self.build_token_provider()?;
376 let client_email = token_provider.service_account_key.client_email.clone();
377 let universe_domain =
378 universe_domain_override.or(token_provider.service_account_key.universe_domain.clone());
379 let access_boundary_url = crate::access_boundary::service_account_lookup_url(
380 &client_email,
381 iam_endpoint.as_deref(),
382 );
383 let creds = ServiceAccountCredentials {
384 quota_project_id,
385 token_provider: TokenCache::new(token_provider),
386 universe_domain,
387 };
388
389 Ok(CredentialsWithAccessBoundary::new(
390 creds,
391 Some(access_boundary_url),
392 ))
393 }
394
395 pub fn build_signer(self) -> BuildResult<crate::signer::Signer> {
423 let service_account_key =
424 serde_json::from_value::<ServiceAccountKey>(self.service_account_key.clone())
425 .map_err(BuilderError::parsing)?;
426 let signing_provider =
427 crate::signer::service_account::ServiceAccountSigner::new(service_account_key);
428 Ok(crate::signer::Signer {
429 inner: Arc::new(signing_provider),
430 })
431 }
432}
433
434#[derive(serde::Deserialize, Default, Clone)]
438pub(crate) struct ServiceAccountKey {
439 pub(crate) client_email: String,
442 private_key_id: String,
444 private_key: String,
447 project_id: String,
449 pub(crate) universe_domain: Option<String>,
451}
452
453impl ServiceAccountKey {
454 pub(crate) fn signer(&self) -> Result<Box<dyn Signer>> {
456 let private_key = self.private_key.clone();
457 let key_provider = crate::credentials::crypto_provider::get_key_provider();
458
459 let key_der = PrivateKeyDer::from_pem_slice(private_key.as_bytes()).map_err(|e| {
460 errors::non_retryable_from_str(format!(
461 "failed to parse service account private key PEM: {}",
462 e
463 ))
464 })?;
465
466 let pkcs8_der = match key_der {
467 PrivateKeyDer::Pkcs8(der) => der,
468 _ => {
469 return Err(errors::non_retryable_from_str(format!(
470 "expected key to be in form of PKCS8, found {:?}",
471 key_der
472 )));
473 }
474 };
475
476 let pk = key_provider
477 .load_private_key(PrivateKeyDer::Pkcs8(pkcs8_der))
478 .map_err(errors::non_retryable)?;
479
480 pk.choose_scheme(&[rustls::SignatureScheme::RSA_PKCS1_SHA256])
481 .ok_or_else(||{
482 errors::non_retryable_from_str("unable to choose RSA_PKCS1_SHA256 signing scheme as it is not supported by current signer")
483 })
484 }
485}
486
487impl std::fmt::Debug for ServiceAccountKey {
488 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
489 f.debug_struct("ServiceAccountKey")
490 .field("client_email", &self.client_email)
491 .field("private_key_id", &self.private_key_id)
492 .field("private_key", &"[censored]")
493 .field("project_id", &self.project_id)
494 .field("universe_domain", &self.universe_domain)
495 .finish()
496 }
497}
498
499#[derive(Debug)]
500struct ServiceAccountCredentials<T>
501where
502 T: CachedTokenProvider,
503{
504 token_provider: T,
505 quota_project_id: Option<String>,
506 universe_domain: Option<String>,
507}
508
509#[derive(Debug)]
510struct ServiceAccountTokenProvider {
511 service_account_key: ServiceAccountKey,
512 access_specifier: AccessSpecifier,
513}
514
515fn token_issue_time(current_time: OffsetDateTime) -> OffsetDateTime {
516 current_time - CLOCK_SKEW_FUDGE
517}
518
519fn token_expiry_time(current_time: OffsetDateTime) -> OffsetDateTime {
520 current_time + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT
521}
522
523#[async_trait]
524impl TokenProvider for ServiceAccountTokenProvider {
525 async fn token(&self) -> Result<Token> {
526 let expires_at = Instant::now() + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT;
527 let tg = ServiceAccountTokenGenerator {
528 audience: self.access_specifier.audience().cloned(),
529 scopes: self
530 .access_specifier
531 .scopes()
532 .map(|scopes| scopes.join(" ")),
533 service_account_key: self.service_account_key.clone(),
534 target_audience: None,
535 };
536
537 let token = tg.generate()?;
538
539 let token = Token {
540 token,
541 token_type: "Bearer".to_string(),
542 expires_at: Some(expires_at),
543 metadata: None,
544 };
545 Ok(token)
546 }
547}
548
549#[derive(Default, Clone)]
550pub(crate) struct ServiceAccountTokenGenerator {
551 service_account_key: ServiceAccountKey,
552 audience: Option<String>,
553 scopes: Option<String>,
554 target_audience: Option<String>,
555}
556
557impl ServiceAccountTokenGenerator {
558 #[cfg(feature = "idtoken")]
559 pub(crate) fn new_id_token_generator(
560 target_audience: String,
561 audience: String,
562 service_account_key: ServiceAccountKey,
563 ) -> Self {
564 Self {
565 service_account_key,
566 target_audience: Some(target_audience),
567 audience: Some(audience),
568 scopes: None,
569 }
570 }
571
572 pub(crate) fn generate(&self) -> Result<String> {
573 let signer = self.service_account_key.signer()?;
574
575 let current_time = OffsetDateTime::now_utc();
579
580 let claims = JwsClaims {
581 iss: self.service_account_key.client_email.clone(),
582 scope: self.scopes.clone(),
583 target_audience: self.target_audience.clone(),
584 aud: self.audience.clone(),
585 exp: token_expiry_time(current_time),
586 iat: token_issue_time(current_time),
587 typ: None,
588 sub: Some(self.service_account_key.client_email.clone()),
589 };
590
591 let header = JwsHeader {
592 alg: "RS256",
593 typ: "JWT",
594 kid: Some(self.service_account_key.private_key_id.clone()),
595 };
596 let encoded_header_claims = format!("{}.{}", header.encode()?, claims.encode()?);
597 let sig = signer
598 .sign(encoded_header_claims.as_bytes())
599 .map_err(errors::non_retryable)?;
600 use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine as _};
601 let token = format!(
602 "{}.{}",
603 encoded_header_claims,
604 &BASE64_URL_SAFE_NO_PAD.encode(sig)
605 );
606
607 Ok(token)
608 }
609}
610
611#[async_trait::async_trait]
612impl<T> CredentialsProvider for ServiceAccountCredentials<T>
613where
614 T: CachedTokenProvider,
615{
616 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
617 let token = self.token_provider.token(extensions).await?;
618
619 AuthHeadersBuilder::new(&token)
620 .maybe_quota_project_id(self.quota_project_id.as_deref())
621 .build()
622 }
623
624 async fn universe_domain(&self) -> Option<String> {
625 self.universe_domain.clone()
626 }
627}
628
629#[async_trait::async_trait]
630impl<T> AccessTokenCredentialsProvider for ServiceAccountCredentials<T>
631where
632 T: CachedTokenProvider,
633{
634 async fn access_token(&self) -> Result<AccessToken> {
635 let token = self.token_provider.token(Extensions::new()).await?;
636 token.into()
637 }
638}
639
640#[cfg(test)]
641mod tests {
642 use super::*;
643 use crate::credentials::QUOTA_PROJECT_KEY;
644 use crate::credentials::tests::{
645 PKCS8_PK, b64_decode_to_json, get_headers_from_cache, get_token_from_headers,
646 };
647 use crate::token::tests::MockTokenProvider;
648 use http::HeaderValue;
649 use http::header::AUTHORIZATION;
650 use rsa::pkcs1::EncodeRsaPrivateKey;
651 use rsa::pkcs8::LineEnding;
652 use serde_json::Value;
653 use serde_json::json;
654 use serial_test::parallel;
655 use std::error::Error as _;
656 use std::time::Duration;
657
658 type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
659
660 const SSJ_REGEX: &str = r"(?<header>[^\.]+)\.(?<claims>[^\.]+)\.(?<sig>[^\.]+)";
661
662 #[test]
663 #[parallel]
664 fn debug_token_provider() {
665 let expected = ServiceAccountKey {
666 client_email: "test-client-email".to_string(),
667 private_key_id: "test-private-key-id".to_string(),
668 private_key: "super-duper-secret-private-key".to_string(),
669 project_id: "test-project-id".to_string(),
670 universe_domain: Some("test-universe-domain".to_string()),
671 };
672 let fmt = format!("{expected:?}");
673 assert!(fmt.contains("test-client-email"), "{fmt}");
674 assert!(fmt.contains("test-private-key-id"), "{fmt}");
675 assert!(!fmt.contains("super-duper-secret-private-key"), "{fmt}");
676 assert!(fmt.contains("test-project-id"), "{fmt}");
677 assert!(fmt.contains("test-universe-domain"), "{fmt}");
678 }
679
680 #[test]
681 #[parallel]
682 fn validate_token_issue_time() {
683 let current_time = OffsetDateTime::now_utc();
684 let token_issue_time = token_issue_time(current_time);
685 assert!(token_issue_time == current_time - CLOCK_SKEW_FUDGE);
686 }
687
688 #[test]
689 #[parallel]
690 fn validate_token_expiry_time() {
691 let current_time = OffsetDateTime::now_utc();
692 let token_issue_time = token_expiry_time(current_time);
693 assert!(token_issue_time == current_time + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT);
694 }
695
696 #[tokio::test]
697 #[parallel]
698 async fn headers_success_without_quota_project() -> TestResult {
699 let token = Token {
700 token: "test-token".to_string(),
701 token_type: "Bearer".to_string(),
702 expires_at: None,
703 metadata: None,
704 };
705
706 let mut mock = MockTokenProvider::new();
707 mock.expect_token().times(1).return_once(|| Ok(token));
708
709 let sac = ServiceAccountCredentials {
710 token_provider: TokenCache::new(mock),
711 quota_project_id: None,
712 universe_domain: None,
713 };
714
715 let mut extensions = Extensions::new();
716 let cached_headers = sac.headers(extensions.clone()).await.unwrap();
717 let (headers, entity_tag) = match cached_headers {
718 CacheableResource::New { entity_tag, data } => (data, entity_tag),
719 CacheableResource::NotModified => unreachable!("expecting new headers"),
720 };
721 let token = headers.get(AUTHORIZATION).unwrap();
722
723 assert_eq!(headers.len(), 1, "{headers:?}");
724 assert_eq!(token, HeaderValue::from_static("Bearer test-token"));
725 assert!(token.is_sensitive());
726
727 extensions.insert(entity_tag);
728
729 let cached_headers = sac.headers(extensions).await?;
730
731 match cached_headers {
732 CacheableResource::New { .. } => unreachable!("expecting new headers"),
733 CacheableResource::NotModified => CacheableResource::<HeaderMap>::NotModified,
734 };
735 Ok(())
736 }
737
738 #[tokio::test]
739 #[parallel]
740 async fn headers_success_with_quota_project() -> TestResult {
741 let token = Token {
742 token: "test-token".to_string(),
743 token_type: "Bearer".to_string(),
744 expires_at: None,
745 metadata: None,
746 };
747
748 let quota_project = "test-quota-project";
749
750 let mut mock = MockTokenProvider::new();
751 mock.expect_token().times(1).return_once(|| Ok(token));
752
753 let sac = ServiceAccountCredentials {
754 token_provider: TokenCache::new(mock),
755 quota_project_id: Some(quota_project.to_string()),
756 universe_domain: None,
757 };
758
759 let headers = get_headers_from_cache(sac.headers(Extensions::new()).await.unwrap())?;
760 let token = headers.get(AUTHORIZATION).unwrap();
761 let quota_project_header = headers.get(QUOTA_PROJECT_KEY).unwrap();
762
763 assert_eq!(headers.len(), 2, "{headers:?}");
764 assert_eq!(token, HeaderValue::from_static("Bearer test-token"));
765 assert!(token.is_sensitive());
766 assert_eq!(
767 quota_project_header,
768 HeaderValue::from_static(quota_project)
769 );
770 assert!(!quota_project_header.is_sensitive());
771 Ok(())
772 }
773
774 #[tokio::test]
775 #[parallel]
776 async fn headers_failure() {
777 let mut mock = MockTokenProvider::new();
778 mock.expect_token()
779 .times(1)
780 .return_once(|| Err(errors::non_retryable_from_str("fail")));
781
782 let sac = ServiceAccountCredentials {
783 token_provider: TokenCache::new(mock),
784 quota_project_id: None,
785 universe_domain: None,
786 };
787 let result = sac.headers(Extensions::new()).await;
788 assert!(result.is_err(), "{result:?}");
789 }
790
791 fn get_mock_service_key() -> Value {
792 json!({
793 "client_email": "test-client-email",
794 "private_key_id": "test-private-key-id",
795 "private_key": "",
796 "project_id": "test-project-id",
797 })
798 }
799
800 #[tokio::test]
801 #[parallel]
802 async fn get_service_account_headers_pkcs1_private_key_failure() -> TestResult {
803 let mut service_account_key = get_mock_service_key();
804
805 let key = crate::credentials::tests::RSA_PRIVATE_KEY
806 .to_pkcs1_pem(LineEnding::LF)
807 .expect("Failed to encode key to PKCS#1 PEM")
808 .to_string();
809
810 service_account_key["private_key"] = Value::from(key);
811 let cred = Builder::new(service_account_key).build()?;
812 let expected_error_message = "expected key to be in form of PKCS8, found ";
813 assert!(
814 cred.headers(Extensions::new())
815 .await
816 .is_err_and(|e| e.to_string().contains(expected_error_message))
817 );
818 Ok(())
819 }
820
821 #[tokio::test]
822 #[parallel]
823 async fn get_service_account_token_pkcs8_key_success() -> TestResult {
824 let mut service_account_key = get_mock_service_key();
825 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
826 let tp = Builder::new(service_account_key.clone()).build_token_provider()?;
827
828 let token = tp.token().await?;
829 let re = regex::Regex::new(SSJ_REGEX).unwrap();
830 let captures = re.captures(&token.token).ok_or_else(|| {
831 format!(
832 r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {}"#,
833 token.token
834 )
835 })?;
836 let header = b64_decode_to_json(captures["header"].to_string());
837 assert_eq!(header["alg"], "RS256");
838 assert_eq!(header["typ"], "JWT");
839 assert_eq!(header["kid"], service_account_key["private_key_id"]);
840
841 let claims = b64_decode_to_json(captures["claims"].to_string());
842 assert_eq!(claims["iss"], service_account_key["client_email"]);
843 assert_eq!(claims["scope"], DEFAULT_SCOPE);
844 assert!(claims["iat"].is_number());
845 assert!(claims["exp"].is_number());
846 assert_eq!(claims["sub"], service_account_key["client_email"]);
847
848 Ok(())
849 }
850
851 #[tokio::test(start_paused = true)]
852 #[parallel]
853 async fn header_caching() -> TestResult {
854 let private_key = PKCS8_PK.clone();
855
856 let json_value = json!({
857 "client_email": "test-client-email",
858 "private_key_id": "test-private-key-id",
859 "private_key": private_key,
860 "project_id": "test-project-id",
861 "universe_domain": "googleapis.com"
862 });
863
864 let credentials = Builder::new(json_value).build()?;
865
866 let headers = credentials.headers(Extensions::new()).await?;
867
868 let re = regex::Regex::new(SSJ_REGEX).unwrap();
869 let token = get_token_from_headers(headers).unwrap();
870
871 let captures = re.captures(&token).unwrap();
872
873 let claims = b64_decode_to_json(captures["claims"].to_string());
874 let first_iat = claims["iat"].as_i64().unwrap();
875
876 tokio::time::advance(Duration::from_secs(1)).await;
880
881 let token = get_token_from_headers(credentials.headers(Extensions::new()).await?).unwrap();
883 let captures = re.captures(&token).unwrap();
884
885 let claims = b64_decode_to_json(captures["claims"].to_string());
886 let second_iat = claims["iat"].as_i64().unwrap();
887
888 assert_eq!(first_iat, second_iat);
891
892 Ok(())
893 }
894
895 #[tokio::test]
896 #[parallel]
897 async fn universe_domain() -> TestResult {
898 let private_key = PKCS8_PK.clone();
899 let json_value = json!({
901 "client_email": "test-client-email",
902 "private_key_id": "test-private-key-id",
903 "private_key": private_key,
904 "project_id": "test-project-id",
905 });
906
907 let credentials = Builder::new(json_value).build()?;
908
909 let universe_domain = credentials.universe_domain().await;
910 assert_eq!(universe_domain, None);
911
912 let json_value = json!({
914 "client_email": "test-client-email",
915 "private_key_id": "test-private-key-id",
916 "private_key": private_key,
917 "project_id": "test-project-id",
918 "universe_domain": "some-universe-domain.com"
919 });
920
921 let credentials = Builder::new(json_value.clone()).build()?;
922
923 let universe_domain = credentials.universe_domain().await;
924 assert_eq!(universe_domain.as_deref(), Some("some-universe-domain.com"));
925
926 let credentials = Builder::new(json_value)
927 .with_universe_domain("other-universe-domain.com")
928 .build()?;
929
930 let universe_domain = credentials.universe_domain().await;
931 assert_eq!(
932 universe_domain.as_deref(),
933 Some("other-universe-domain.com")
934 );
935
936 Ok(())
937 }
938
939 #[tokio::test]
940 #[parallel]
941 async fn get_service_account_headers_invalid_key_failure() -> TestResult {
942 let mut service_account_key = get_mock_service_key();
943 let pem_data = "-----BEGIN PRIVATE KEY-----\nMIGkAg==\n-----END PRIVATE KEY-----";
944 service_account_key["private_key"] = Value::from(pem_data);
945 let cred = Builder::new(service_account_key).build()?;
946
947 let token = cred.headers(Extensions::new()).await;
948 let err = token.unwrap_err();
949 assert!(!err.is_transient(), "{err:?}");
950 let source = err.source().and_then(|e| e.downcast_ref::<rustls::Error>());
951 assert!(matches!(source, Some(rustls::Error::General(_))), "{err:?}");
952 Ok(())
953 }
954
955 #[tokio::test]
956 #[parallel]
957 async fn get_service_account_invalid_json_failure() -> TestResult {
958 let service_account_key = Value::from(" ");
959 let e = Builder::new(service_account_key).build().unwrap_err();
960 assert!(e.is_parsing(), "{e:?}");
961
962 Ok(())
963 }
964
965 #[test]
966 fn signer_failure() -> TestResult {
967 let tp = Builder::new(get_mock_service_key()).build_token_provider()?;
968 let tg = ServiceAccountTokenGenerator {
969 service_account_key: tp.service_account_key.clone(),
970 ..Default::default()
971 };
972
973 let signer = tg.service_account_key.signer();
974 let expected_error_message = "failed to parse service account private key PEM";
975 assert!(signer.is_err_and(|e| e.to_string().contains(expected_error_message)));
976 Ok(())
977 }
978
979 #[test]
980 fn signer_fails_on_invalid_pem_type() -> TestResult {
981 let invalid_pem = concat!(
982 "-----BEGI X509 CRL-----\n",
983 "MIIBmzCBja... (truncated) ...\n",
984 "-----END X509 CRL-----"
985 );
986
987 let mut key = ServiceAccountKey {
988 private_key: invalid_pem.to_string(),
989 ..Default::default()
990 };
991 key.private_key = invalid_pem.to_string();
992 let result = key.signer();
993 assert!(result.is_err(), "{result:?}");
994 let error_msg = result.unwrap_err().to_string();
995 assert!(error_msg.contains("failed to parse service account private key PEM"));
996 Ok(())
997 }
998
999 #[tokio::test]
1000 #[parallel]
1001 async fn get_service_account_headers_with_audience() -> TestResult {
1002 let mut service_account_key = get_mock_service_key();
1003 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1004 let headers = Builder::new(service_account_key.clone())
1005 .with_access_specifier(AccessSpecifier::from_audience("test-audience"))
1006 .build()?
1007 .headers(Extensions::new())
1008 .await?;
1009
1010 let re = regex::Regex::new(SSJ_REGEX).unwrap();
1011 let token = get_token_from_headers(headers).unwrap();
1012 let captures = re.captures(&token).ok_or_else(|| {
1013 format!(r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {token}"#)
1014 })?;
1015 let token_header = b64_decode_to_json(captures["header"].to_string());
1016 assert_eq!(token_header["alg"], "RS256");
1017 assert_eq!(token_header["typ"], "JWT");
1018 assert_eq!(token_header["kid"], service_account_key["private_key_id"]);
1019
1020 let claims = b64_decode_to_json(captures["claims"].to_string());
1021 assert_eq!(claims["iss"], service_account_key["client_email"]);
1022 assert_eq!(claims["scope"], Value::Null);
1023 assert_eq!(claims["aud"], "test-audience");
1024 assert!(claims["iat"].is_number());
1025 assert!(claims["exp"].is_number());
1026 assert_eq!(claims["sub"], service_account_key["client_email"]);
1027 Ok(())
1028 }
1029
1030 #[tokio::test(start_paused = true)]
1031 #[parallel]
1032 async fn get_service_account_token_verify_expiry_time() -> TestResult {
1033 let now = Instant::now();
1034 let mut service_account_key = get_mock_service_key();
1035 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1036 let token = Builder::new(service_account_key)
1037 .build_token_provider()?
1038 .token()
1039 .await?;
1040
1041 let expected_expiry = now + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT;
1042
1043 assert_eq!(token.expires_at.unwrap(), expected_expiry);
1044 Ok(())
1045 }
1046
1047 #[tokio::test]
1048 #[parallel]
1049 async fn get_service_account_headers_with_custom_scopes() -> TestResult {
1050 let mut service_account_key = get_mock_service_key();
1051 let scopes = vec![
1052 "https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate",
1053 ];
1054 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1055 let headers = Builder::new(service_account_key.clone())
1056 .with_access_specifier(AccessSpecifier::from_scopes(scopes.clone()))
1057 .build()?
1058 .headers(Extensions::new())
1059 .await?;
1060
1061 let re = regex::Regex::new(SSJ_REGEX).unwrap();
1062 let token = get_token_from_headers(headers).unwrap();
1063 let captures = re.captures(&token).ok_or_else(|| {
1064 format!(r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {token}"#)
1065 })?;
1066 let token_header = b64_decode_to_json(captures["header"].to_string());
1067 assert_eq!(token_header["alg"], "RS256");
1068 assert_eq!(token_header["typ"], "JWT");
1069 assert_eq!(token_header["kid"], service_account_key["private_key_id"]);
1070
1071 let claims = b64_decode_to_json(captures["claims"].to_string());
1072 assert_eq!(claims["iss"], service_account_key["client_email"]);
1073 assert_eq!(claims["scope"], scopes.join(" "));
1074 assert_eq!(claims["aud"], Value::Null);
1075 assert!(claims["iat"].is_number());
1076 assert!(claims["exp"].is_number());
1077 assert_eq!(claims["sub"], service_account_key["client_email"]);
1078 Ok(())
1079 }
1080
1081 #[tokio::test]
1082 #[parallel]
1083 async fn get_service_account_access_token() -> TestResult {
1084 let mut service_account_key = get_mock_service_key();
1085 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1086 let creds = Builder::new(service_account_key.clone()).build_access_token_credentials()?;
1087
1088 let access_token = creds.access_token().await?;
1089 let token = access_token.token;
1090
1091 let re = regex::Regex::new(SSJ_REGEX).unwrap();
1092 let captures = re.captures(&token).ok_or_else(|| {
1093 format!(r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {token}"#)
1094 })?;
1095 let token_header = b64_decode_to_json(captures["header"].to_string());
1096 assert_eq!(token_header["alg"], "RS256");
1097 assert_eq!(token_header["typ"], "JWT");
1098 assert_eq!(token_header["kid"], service_account_key["private_key_id"]);
1099
1100 Ok(())
1101 }
1102
1103 #[tokio::test]
1104 #[parallel]
1105 async fn get_service_account_signer() -> TestResult {
1106 let mut service_account_key = get_mock_service_key();
1107 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1108 let signer = Builder::new(service_account_key.clone()).build_signer()?;
1109
1110 let client_email = signer.client_email().await?;
1111 assert_eq!(client_email, service_account_key["client_email"]);
1112
1113 let _bytes = signer.sign(b"test").await?;
1114
1115 Ok(())
1116 }
1117
1118 #[tokio::test]
1119 #[parallel]
1120 #[cfg(google_cloud_unstable_trusted_boundaries)]
1121 async fn e2e_access_boundary() -> TestResult {
1122 use crate::credentials::tests::get_access_boundary_from_headers;
1123 use httptest::responders::json_encoded;
1124 use httptest::{Expectation, Server, matchers::*};
1125 use serde_json::Value;
1126
1127 let mut service_account_key = get_mock_service_key();
1128 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1129 let email = service_account_key["client_email"].as_str().unwrap();
1130
1131 let server = Server::run();
1132 server.expect(
1133 Expectation::matching(all_of![request::method_path(
1134 "GET",
1135 format!("/v1/projects/-/serviceAccounts/{email}/allowedLocations")
1136 ),])
1137 .times(1)
1138 .respond_with(json_encoded(json!({
1139 "locations": ["us-central1", "us-east1"],
1140 "encodedLocations": "0x1234"
1141 }))),
1142 );
1143
1144 let iam_endpoint = server.url("").to_string().trim_end_matches('/').to_string();
1145
1146 let creds = Builder::new(service_account_key.clone())
1147 .maybe_iam_endpoint_override(Some(iam_endpoint))
1148 .build_credentials()?;
1149
1150 creds.wait_for_boundary().await;
1152
1153 let headers = creds.headers(Extensions::new()).await?;
1154 let token = get_token_from_headers(headers.clone());
1155 let access_boundary = get_access_boundary_from_headers(headers);
1156 assert!(token.is_some(), "should have some token: {token:?}");
1157 assert_eq!(
1158 access_boundary.as_deref(),
1159 Some("0x1234"),
1160 "should be 0x1234 but found: {access_boundary:?}"
1161 );
1162
1163 Ok(())
1164 }
1165}