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