1pub(crate) mod jws;
74
75use crate::access_boundary::CredentialsWithAccessBoundary;
76use crate::build_errors::Error as BuilderError;
77use crate::constants::DEFAULT_SCOPE;
78use crate::credentials::dynamic::{AccessTokenCredentialsProvider, CredentialsProvider};
79use crate::credentials::{AccessToken, AccessTokenCredentials, CacheableResource, Credentials};
80use crate::errors::{self};
81use crate::headers_util::AuthHeadersBuilder;
82use crate::token::{CachedTokenProvider, Token, TokenProvider};
83use crate::token_cache::TokenCache;
84use crate::{BuildResult, Result};
85use async_trait::async_trait;
86use http::{Extensions, HeaderMap};
87use jws::{CLOCK_SKEW_FUDGE, DEFAULT_TOKEN_TIMEOUT, JwsClaims, JwsHeader};
88use rustls::crypto::CryptoProvider;
89use rustls::sign::Signer;
90use rustls_pki_types::{PrivateKeyDer, pem::PemObject};
91use serde_json::Value;
92use std::sync::Arc;
93use time::OffsetDateTime;
94use tokio::time::Instant;
95
96#[derive(Clone, Debug, PartialEq)]
105pub enum AccessSpecifier {
106 Audience(String),
113
114 Scopes(Vec<String>),
133}
134
135impl AccessSpecifier {
136 fn audience(&self) -> Option<&String> {
137 match self {
138 AccessSpecifier::Audience(aud) => Some(aud),
139 AccessSpecifier::Scopes(_) => None,
140 }
141 }
142
143 fn scopes(&self) -> Option<&[String]> {
144 match self {
145 AccessSpecifier::Scopes(scopes) => Some(scopes),
146 AccessSpecifier::Audience(_) => None,
147 }
148 }
149
150 pub fn from_scopes<I, S>(scopes: I) -> Self
164 where
165 I: IntoIterator<Item = S>,
166 S: Into<String>,
167 {
168 AccessSpecifier::Scopes(scopes.into_iter().map(|s| s.into()).collect())
169 }
170
171 pub fn from_audience<S: Into<String>>(audience: S) -> Self {
185 AccessSpecifier::Audience(audience.into())
186 }
187}
188
189pub struct Builder {
208 service_account_key: Value,
209 access_specifier: AccessSpecifier,
210 quota_project_id: 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 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 #[cfg(all(test, google_cloud_unstable_trusted_boundaries))]
270 fn maybe_iam_endpoint_override(mut self, iam_endpoint_override: Option<String>) -> Self {
271 self.iam_endpoint_override = iam_endpoint_override;
272 self
273 }
274
275 fn build_token_provider(self) -> BuildResult<ServiceAccountTokenProvider> {
276 let service_account_key =
277 serde_json::from_value::<ServiceAccountKey>(self.service_account_key)
278 .map_err(BuilderError::parsing)?;
279
280 Ok(ServiceAccountTokenProvider {
281 service_account_key,
282 access_specifier: self.access_specifier,
283 })
284 }
285
286 pub fn build(self) -> BuildResult<Credentials> {
300 Ok(self.build_credentials()?.into())
301 }
302
303 pub fn build_access_token_credentials(self) -> BuildResult<AccessTokenCredentials> {
340 Ok(self.build_credentials()?.into())
341 }
342
343 fn build_credentials(
344 self,
345 ) -> BuildResult<CredentialsWithAccessBoundary<ServiceAccountCredentials<TokenCache>>> {
346 let iam_endpoint = self.iam_endpoint_override.clone();
347 let quota_project_id = self.quota_project_id.clone();
348 let token_provider = self.build_token_provider()?;
349 let client_email = token_provider.service_account_key.client_email.clone();
350 let access_boundary_url = crate::access_boundary::service_account_lookup_url(
351 &client_email,
352 iam_endpoint.as_deref(),
353 );
354 let creds = ServiceAccountCredentials {
355 quota_project_id,
356 token_provider: TokenCache::new(token_provider),
357 };
358
359 Ok(CredentialsWithAccessBoundary::new(
360 creds,
361 Some(access_boundary_url),
362 ))
363 }
364
365 pub fn build_signer(self) -> BuildResult<crate::signer::Signer> {
394 let service_account_key =
395 serde_json::from_value::<ServiceAccountKey>(self.service_account_key.clone())
396 .map_err(BuilderError::parsing)?;
397 let signing_provider =
398 crate::signer::service_account::ServiceAccountSigner::new(service_account_key);
399 Ok(crate::signer::Signer {
400 inner: Arc::new(signing_provider),
401 })
402 }
403}
404
405#[derive(serde::Deserialize, Default, Clone)]
409pub(crate) struct ServiceAccountKey {
410 pub(crate) client_email: String,
413 private_key_id: String,
415 private_key: String,
418 project_id: String,
420 universe_domain: Option<String>,
422}
423
424impl ServiceAccountKey {
425 pub(crate) fn signer(&self) -> Result<Box<dyn Signer>> {
427 let private_key = self.private_key.clone();
428 let key_provider = CryptoProvider::get_default().map(|p| p.key_provider);
429 #[cfg(feature = "default-rustls-provider")]
430 let key_provider = key_provider
431 .unwrap_or_else(|| rustls::crypto::aws_lc_rs::default_provider().key_provider);
432 #[cfg(not(feature = "default-rustls-provider"))]
433 let key_provider = key_provider.expect(
434 r###"
435The default rustls::CryptoProvider should be configured by the application. The
436`google-cloud-auth` crate was compiled without the `default-rustls-provider`
437feature. Without this feature the crate expects the application to initialize
438the rustls crypto provider using `rustls::CryptoProvider::install_default()`.
439
440Note that the application must use the exact same version of `rustls` as the
441`google-cloud-auth` crate does. Otherwise `install_default()` has no effect."###,
442 );
443
444 let key_der = PrivateKeyDer::from_pem_slice(private_key.as_bytes()).map_err(|e| {
445 errors::non_retryable_from_str(format!(
446 "Failed to parse service account private key PEM: {}",
447 e
448 ))
449 })?;
450
451 let pkcs8_der = match key_der {
452 PrivateKeyDer::Pkcs8(der) => der,
453 _ => {
454 return Err(errors::non_retryable_from_str(format!(
455 "expected key to be in form of PKCS8, found {:?}",
456 key_der
457 )));
458 }
459 };
460
461 let pk = key_provider
462 .load_private_key(PrivateKeyDer::Pkcs8(pkcs8_der))
463 .map_err(errors::non_retryable)?;
464
465 pk.choose_scheme(&[rustls::SignatureScheme::RSA_PKCS1_SHA256])
466 .ok_or_else(||{
467 errors::non_retryable_from_str("Unable to choose RSA_PKCS1_SHA256 signing scheme as it is not supported by current signer")
468 })
469 }
470}
471
472impl std::fmt::Debug for ServiceAccountKey {
473 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
474 f.debug_struct("ServiceAccountKey")
475 .field("client_email", &self.client_email)
476 .field("private_key_id", &self.private_key_id)
477 .field("private_key", &"[censored]")
478 .field("project_id", &self.project_id)
479 .field("universe_domain", &self.universe_domain)
480 .finish()
481 }
482}
483
484#[derive(Debug)]
485struct ServiceAccountCredentials<T>
486where
487 T: CachedTokenProvider,
488{
489 token_provider: T,
490 quota_project_id: Option<String>,
491}
492
493#[derive(Debug)]
494struct ServiceAccountTokenProvider {
495 service_account_key: ServiceAccountKey,
496 access_specifier: AccessSpecifier,
497}
498
499fn token_issue_time(current_time: OffsetDateTime) -> OffsetDateTime {
500 current_time - CLOCK_SKEW_FUDGE
501}
502
503fn token_expiry_time(current_time: OffsetDateTime) -> OffsetDateTime {
504 current_time + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT
505}
506
507#[async_trait]
508impl TokenProvider for ServiceAccountTokenProvider {
509 async fn token(&self) -> Result<Token> {
510 let expires_at = Instant::now() + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT;
511 let tg = ServiceAccountTokenGenerator {
512 audience: self.access_specifier.audience().cloned(),
513 scopes: self
514 .access_specifier
515 .scopes()
516 .map(|scopes| scopes.join(" ")),
517 service_account_key: self.service_account_key.clone(),
518 target_audience: None,
519 };
520
521 let token = tg.generate()?;
522
523 let token = Token {
524 token,
525 token_type: "Bearer".to_string(),
526 expires_at: Some(expires_at),
527 metadata: None,
528 };
529 Ok(token)
530 }
531}
532
533#[derive(Default, Clone)]
534pub(crate) struct ServiceAccountTokenGenerator {
535 service_account_key: ServiceAccountKey,
536 audience: Option<String>,
537 scopes: Option<String>,
538 target_audience: Option<String>,
539}
540
541impl ServiceAccountTokenGenerator {
542 #[cfg(feature = "idtoken")]
543 pub(crate) fn new_id_token_generator(
544 target_audience: String,
545 audience: String,
546 service_account_key: ServiceAccountKey,
547 ) -> Self {
548 Self {
549 service_account_key,
550 target_audience: Some(target_audience),
551 audience: Some(audience),
552 scopes: None,
553 }
554 }
555
556 pub(crate) fn generate(&self) -> Result<String> {
557 let signer = self.service_account_key.signer()?;
558
559 let current_time = OffsetDateTime::now_utc();
563
564 let claims = JwsClaims {
565 iss: self.service_account_key.client_email.clone(),
566 scope: self.scopes.clone(),
567 target_audience: self.target_audience.clone(),
568 aud: self.audience.clone(),
569 exp: token_expiry_time(current_time),
570 iat: token_issue_time(current_time),
571 typ: None,
572 sub: Some(self.service_account_key.client_email.clone()),
573 };
574
575 let header = JwsHeader {
576 alg: "RS256",
577 typ: "JWT",
578 kid: Some(self.service_account_key.private_key_id.clone()),
579 };
580 let encoded_header_claims = format!("{}.{}", header.encode()?, claims.encode()?);
581 let sig = signer
582 .sign(encoded_header_claims.as_bytes())
583 .map_err(errors::non_retryable)?;
584 use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine as _};
585 let token = format!(
586 "{}.{}",
587 encoded_header_claims,
588 &BASE64_URL_SAFE_NO_PAD.encode(sig)
589 );
590
591 Ok(token)
592 }
593}
594
595#[async_trait::async_trait]
596impl<T> CredentialsProvider for ServiceAccountCredentials<T>
597where
598 T: CachedTokenProvider,
599{
600 async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
601 let token = self.token_provider.token(extensions).await?;
602
603 AuthHeadersBuilder::new(&token)
604 .maybe_quota_project_id(self.quota_project_id.as_deref())
605 .build()
606 }
607}
608
609#[async_trait::async_trait]
610impl<T> AccessTokenCredentialsProvider for ServiceAccountCredentials<T>
611where
612 T: CachedTokenProvider,
613{
614 async fn access_token(&self) -> Result<AccessToken> {
615 let token = self.token_provider.token(Extensions::new()).await?;
616 token.into()
617 }
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623 use crate::credentials::QUOTA_PROJECT_KEY;
624 use crate::credentials::tests::{
625 PKCS8_PK, b64_decode_to_json, get_headers_from_cache, get_token_from_headers,
626 };
627 use crate::token::tests::MockTokenProvider;
628 use http::HeaderValue;
629 use http::header::AUTHORIZATION;
630 use rsa::pkcs1::EncodeRsaPrivateKey;
631 use rsa::pkcs8::LineEnding;
632 use serde_json::Value;
633 use serde_json::json;
634 use serial_test::parallel;
635 use std::error::Error as _;
636 use std::time::Duration;
637
638 type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
639
640 const SSJ_REGEX: &str = r"(?<header>[^\.]+)\.(?<claims>[^\.]+)\.(?<sig>[^\.]+)";
641
642 #[test]
643 #[parallel]
644 fn debug_token_provider() {
645 let expected = ServiceAccountKey {
646 client_email: "test-client-email".to_string(),
647 private_key_id: "test-private-key-id".to_string(),
648 private_key: "super-duper-secret-private-key".to_string(),
649 project_id: "test-project-id".to_string(),
650 universe_domain: Some("test-universe-domain".to_string()),
651 };
652 let fmt = format!("{expected:?}");
653 assert!(fmt.contains("test-client-email"), "{fmt}");
654 assert!(fmt.contains("test-private-key-id"), "{fmt}");
655 assert!(!fmt.contains("super-duper-secret-private-key"), "{fmt}");
656 assert!(fmt.contains("test-project-id"), "{fmt}");
657 assert!(fmt.contains("test-universe-domain"), "{fmt}");
658 }
659
660 #[test]
661 #[parallel]
662 fn validate_token_issue_time() {
663 let current_time = OffsetDateTime::now_utc();
664 let token_issue_time = token_issue_time(current_time);
665 assert!(token_issue_time == current_time - CLOCK_SKEW_FUDGE);
666 }
667
668 #[test]
669 #[parallel]
670 fn validate_token_expiry_time() {
671 let current_time = OffsetDateTime::now_utc();
672 let token_issue_time = token_expiry_time(current_time);
673 assert!(token_issue_time == current_time + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT);
674 }
675
676 #[tokio::test]
677 #[parallel]
678 async fn headers_success_without_quota_project() -> TestResult {
679 let token = Token {
680 token: "test-token".to_string(),
681 token_type: "Bearer".to_string(),
682 expires_at: None,
683 metadata: None,
684 };
685
686 let mut mock = MockTokenProvider::new();
687 mock.expect_token().times(1).return_once(|| Ok(token));
688
689 let sac = ServiceAccountCredentials {
690 token_provider: TokenCache::new(mock),
691 quota_project_id: None,
692 };
693
694 let mut extensions = Extensions::new();
695 let cached_headers = sac.headers(extensions.clone()).await.unwrap();
696 let (headers, entity_tag) = match cached_headers {
697 CacheableResource::New { entity_tag, data } => (data, entity_tag),
698 CacheableResource::NotModified => unreachable!("expecting new headers"),
699 };
700 let token = headers.get(AUTHORIZATION).unwrap();
701
702 assert_eq!(headers.len(), 1, "{headers:?}");
703 assert_eq!(token, HeaderValue::from_static("Bearer test-token"));
704 assert!(token.is_sensitive());
705
706 extensions.insert(entity_tag);
707
708 let cached_headers = sac.headers(extensions).await?;
709
710 match cached_headers {
711 CacheableResource::New { .. } => unreachable!("expecting new headers"),
712 CacheableResource::NotModified => CacheableResource::<HeaderMap>::NotModified,
713 };
714 Ok(())
715 }
716
717 #[tokio::test]
718 #[parallel]
719 async fn headers_success_with_quota_project() -> TestResult {
720 let token = Token {
721 token: "test-token".to_string(),
722 token_type: "Bearer".to_string(),
723 expires_at: None,
724 metadata: None,
725 };
726
727 let quota_project = "test-quota-project";
728
729 let mut mock = MockTokenProvider::new();
730 mock.expect_token().times(1).return_once(|| Ok(token));
731
732 let sac = ServiceAccountCredentials {
733 token_provider: TokenCache::new(mock),
734 quota_project_id: Some(quota_project.to_string()),
735 };
736
737 let headers = get_headers_from_cache(sac.headers(Extensions::new()).await.unwrap())?;
738 let token = headers.get(AUTHORIZATION).unwrap();
739 let quota_project_header = headers.get(QUOTA_PROJECT_KEY).unwrap();
740
741 assert_eq!(headers.len(), 2, "{headers:?}");
742 assert_eq!(token, HeaderValue::from_static("Bearer test-token"));
743 assert!(token.is_sensitive());
744 assert_eq!(
745 quota_project_header,
746 HeaderValue::from_static(quota_project)
747 );
748 assert!(!quota_project_header.is_sensitive());
749 Ok(())
750 }
751
752 #[tokio::test]
753 #[parallel]
754 async fn headers_failure() {
755 let mut mock = MockTokenProvider::new();
756 mock.expect_token()
757 .times(1)
758 .return_once(|| Err(errors::non_retryable_from_str("fail")));
759
760 let sac = ServiceAccountCredentials {
761 token_provider: TokenCache::new(mock),
762 quota_project_id: None,
763 };
764 let result = sac.headers(Extensions::new()).await;
765 assert!(result.is_err(), "{result:?}");
766 }
767
768 fn get_mock_service_key() -> Value {
769 json!({
770 "client_email": "test-client-email",
771 "private_key_id": "test-private-key-id",
772 "private_key": "",
773 "project_id": "test-project-id",
774 })
775 }
776
777 #[tokio::test]
778 #[parallel]
779 async fn get_service_account_headers_pkcs1_private_key_failure() -> TestResult {
780 let mut service_account_key = get_mock_service_key();
781
782 let key = crate::credentials::tests::RSA_PRIVATE_KEY
783 .to_pkcs1_pem(LineEnding::LF)
784 .expect("Failed to encode key to PKCS#1 PEM")
785 .to_string();
786
787 service_account_key["private_key"] = Value::from(key);
788 let cred = Builder::new(service_account_key).build()?;
789 let expected_error_message = "expected key to be in form of PKCS8, found ";
790 assert!(
791 cred.headers(Extensions::new())
792 .await
793 .is_err_and(|e| e.to_string().contains(expected_error_message))
794 );
795 Ok(())
796 }
797
798 #[tokio::test]
799 #[parallel]
800 async fn get_service_account_token_pkcs8_key_success() -> TestResult {
801 let mut service_account_key = get_mock_service_key();
802 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
803 let tp = Builder::new(service_account_key.clone()).build_token_provider()?;
804
805 let token = tp.token().await?;
806 let re = regex::Regex::new(SSJ_REGEX).unwrap();
807 let captures = re.captures(&token.token).ok_or_else(|| {
808 format!(
809 r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {}"#,
810 token.token
811 )
812 })?;
813 let header = b64_decode_to_json(captures["header"].to_string());
814 assert_eq!(header["alg"], "RS256");
815 assert_eq!(header["typ"], "JWT");
816 assert_eq!(header["kid"], service_account_key["private_key_id"]);
817
818 let claims = b64_decode_to_json(captures["claims"].to_string());
819 assert_eq!(claims["iss"], service_account_key["client_email"]);
820 assert_eq!(claims["scope"], DEFAULT_SCOPE);
821 assert!(claims["iat"].is_number());
822 assert!(claims["exp"].is_number());
823 assert_eq!(claims["sub"], service_account_key["client_email"]);
824
825 Ok(())
826 }
827
828 #[tokio::test]
829 #[parallel]
830 async fn header_caching() -> TestResult {
831 let private_key = PKCS8_PK.clone();
832
833 let json_value = json!({
834 "client_email": "test-client-email",
835 "private_key_id": "test-private-key-id",
836 "private_key": private_key,
837 "project_id": "test-project-id",
838 "universe_domain": "test-universe-domain"
839 });
840
841 let credentials = Builder::new(json_value).build()?;
842
843 let headers = credentials.headers(Extensions::new()).await?;
844
845 let re = regex::Regex::new(SSJ_REGEX).unwrap();
846 let token = get_token_from_headers(headers).unwrap();
847
848 let captures = re.captures(&token).unwrap();
849
850 let claims = b64_decode_to_json(captures["claims"].to_string());
851 let first_iat = claims["iat"].as_i64().unwrap();
852
853 std::thread::sleep(Duration::from_secs(1));
858
859 let token = get_token_from_headers(credentials.headers(Extensions::new()).await?).unwrap();
861 let captures = re.captures(&token).unwrap();
862
863 let claims = b64_decode_to_json(captures["claims"].to_string());
864 let second_iat = claims["iat"].as_i64().unwrap();
865
866 assert_eq!(first_iat, second_iat);
869
870 Ok(())
871 }
872
873 #[tokio::test]
874 #[parallel]
875 async fn get_service_account_headers_invalid_key_failure() -> TestResult {
876 let mut service_account_key = get_mock_service_key();
877 let pem_data = "-----BEGIN PRIVATE KEY-----\nMIGkAg==\n-----END PRIVATE KEY-----";
878 service_account_key["private_key"] = Value::from(pem_data);
879 let cred = Builder::new(service_account_key).build()?;
880
881 let token = cred.headers(Extensions::new()).await;
882 let err = token.unwrap_err();
883 assert!(!err.is_transient(), "{err:?}");
884 let source = err.source().and_then(|e| e.downcast_ref::<rustls::Error>());
885 assert!(matches!(source, Some(rustls::Error::General(_))), "{err:?}");
886 Ok(())
887 }
888
889 #[tokio::test]
890 #[parallel]
891 async fn get_service_account_invalid_json_failure() -> TestResult {
892 let service_account_key = Value::from(" ");
893 let e = Builder::new(service_account_key).build().unwrap_err();
894 assert!(e.is_parsing(), "{e:?}");
895
896 Ok(())
897 }
898
899 #[test]
900 fn signer_failure() -> TestResult {
901 let tp = Builder::new(get_mock_service_key()).build_token_provider()?;
902 let tg = ServiceAccountTokenGenerator {
903 service_account_key: tp.service_account_key.clone(),
904 ..Default::default()
905 };
906
907 let signer = tg.service_account_key.signer();
908 let expected_error_message = "Failed to parse service account private key PEM";
909 assert!(signer.is_err_and(|e| e.to_string().contains(expected_error_message)));
910 Ok(())
911 }
912
913 #[test]
914 fn signer_fails_on_invalid_pem_type() -> TestResult {
915 let invalid_pem = concat!(
916 "-----BEGI X509 CRL-----\n",
917 "MIIBmzCBja... (truncated) ...\n",
918 "-----END X509 CRL-----"
919 );
920
921 let mut key = ServiceAccountKey {
922 private_key: invalid_pem.to_string(),
923 ..Default::default()
924 };
925 key.private_key = invalid_pem.to_string();
926 let result = key.signer();
927 assert!(result.is_err(), "{result:?}");
928 let error_msg = result.unwrap_err().to_string();
929 assert!(error_msg.contains("Failed to parse service account private key PEM"));
930 Ok(())
931 }
932
933 #[tokio::test]
934 #[parallel]
935 async fn get_service_account_headers_with_audience() -> TestResult {
936 let mut service_account_key = get_mock_service_key();
937 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
938 let headers = Builder::new(service_account_key.clone())
939 .with_access_specifier(AccessSpecifier::from_audience("test-audience"))
940 .build()?
941 .headers(Extensions::new())
942 .await?;
943
944 let re = regex::Regex::new(SSJ_REGEX).unwrap();
945 let token = get_token_from_headers(headers).unwrap();
946 let captures = re.captures(&token).ok_or_else(|| {
947 format!(r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {token}"#)
948 })?;
949 let token_header = b64_decode_to_json(captures["header"].to_string());
950 assert_eq!(token_header["alg"], "RS256");
951 assert_eq!(token_header["typ"], "JWT");
952 assert_eq!(token_header["kid"], service_account_key["private_key_id"]);
953
954 let claims = b64_decode_to_json(captures["claims"].to_string());
955 assert_eq!(claims["iss"], service_account_key["client_email"]);
956 assert_eq!(claims["scope"], Value::Null);
957 assert_eq!(claims["aud"], "test-audience");
958 assert!(claims["iat"].is_number());
959 assert!(claims["exp"].is_number());
960 assert_eq!(claims["sub"], service_account_key["client_email"]);
961 Ok(())
962 }
963
964 #[tokio::test(start_paused = true)]
965 #[parallel]
966 async fn get_service_account_token_verify_expiry_time() -> TestResult {
967 let now = Instant::now();
968 let mut service_account_key = get_mock_service_key();
969 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
970 let token = Builder::new(service_account_key)
971 .build_token_provider()?
972 .token()
973 .await?;
974
975 let expected_expiry = now + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT;
976
977 assert_eq!(token.expires_at.unwrap(), expected_expiry);
978 Ok(())
979 }
980
981 #[tokio::test]
982 #[parallel]
983 async fn get_service_account_headers_with_custom_scopes() -> TestResult {
984 let mut service_account_key = get_mock_service_key();
985 let scopes = vec![
986 "https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate",
987 ];
988 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
989 let headers = Builder::new(service_account_key.clone())
990 .with_access_specifier(AccessSpecifier::from_scopes(scopes.clone()))
991 .build()?
992 .headers(Extensions::new())
993 .await?;
994
995 let re = regex::Regex::new(SSJ_REGEX).unwrap();
996 let token = get_token_from_headers(headers).unwrap();
997 let captures = re.captures(&token).ok_or_else(|| {
998 format!(r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {token}"#)
999 })?;
1000 let token_header = b64_decode_to_json(captures["header"].to_string());
1001 assert_eq!(token_header["alg"], "RS256");
1002 assert_eq!(token_header["typ"], "JWT");
1003 assert_eq!(token_header["kid"], service_account_key["private_key_id"]);
1004
1005 let claims = b64_decode_to_json(captures["claims"].to_string());
1006 assert_eq!(claims["iss"], service_account_key["client_email"]);
1007 assert_eq!(claims["scope"], scopes.join(" "));
1008 assert_eq!(claims["aud"], Value::Null);
1009 assert!(claims["iat"].is_number());
1010 assert!(claims["exp"].is_number());
1011 assert_eq!(claims["sub"], service_account_key["client_email"]);
1012 Ok(())
1013 }
1014
1015 #[tokio::test]
1016 #[parallel]
1017 async fn get_service_account_access_token() -> TestResult {
1018 let mut service_account_key = get_mock_service_key();
1019 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1020 let creds = Builder::new(service_account_key.clone()).build_access_token_credentials()?;
1021
1022 let access_token = creds.access_token().await?;
1023 let token = access_token.token;
1024
1025 let re = regex::Regex::new(SSJ_REGEX).unwrap();
1026 let captures = re.captures(&token).ok_or_else(|| {
1027 format!(r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {token}"#)
1028 })?;
1029 let token_header = b64_decode_to_json(captures["header"].to_string());
1030 assert_eq!(token_header["alg"], "RS256");
1031 assert_eq!(token_header["typ"], "JWT");
1032 assert_eq!(token_header["kid"], service_account_key["private_key_id"]);
1033
1034 Ok(())
1035 }
1036
1037 #[tokio::test]
1038 #[parallel]
1039 async fn get_service_account_signer() -> TestResult {
1040 let mut service_account_key = get_mock_service_key();
1041 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1042 let signer = Builder::new(service_account_key.clone()).build_signer()?;
1043
1044 let client_email = signer.client_email().await?;
1045 assert_eq!(client_email, service_account_key["client_email"]);
1046
1047 let _bytes = signer.sign(b"test").await?;
1048
1049 Ok(())
1050 }
1051
1052 #[tokio::test]
1053 #[parallel]
1054 #[cfg(google_cloud_unstable_trusted_boundaries)]
1055 async fn e2e_access_boundary() -> TestResult {
1056 use crate::credentials::tests::get_access_boundary_from_headers;
1057 use httptest::responders::json_encoded;
1058 use httptest::{Expectation, Server, matchers::*};
1059 use serde_json::Value;
1060
1061 let mut service_account_key = get_mock_service_key();
1062 service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1063 let email = service_account_key["client_email"].as_str().unwrap();
1064
1065 let server = Server::run();
1066 server.expect(
1067 Expectation::matching(all_of![request::method_path(
1068 "GET",
1069 format!("/v1/projects/-/serviceAccounts/{email}/allowedLocations")
1070 ),])
1071 .times(1)
1072 .respond_with(json_encoded(json!({
1073 "locations": ["us-central1", "us-east1"],
1074 "encodedLocations": "0x1234"
1075 }))),
1076 );
1077
1078 let iam_endpoint = server.url("").to_string().trim_end_matches('/').to_string();
1079
1080 let creds = Builder::new(service_account_key.clone())
1081 .maybe_iam_endpoint_override(Some(iam_endpoint))
1082 .build_credentials()?;
1083
1084 creds.wait_for_boundary().await;
1086
1087 let headers = creds.headers(Extensions::new()).await?;
1088 let token = get_token_from_headers(headers.clone());
1089 let access_boundary = get_access_boundary_from_headers(headers);
1090 assert!(token.is_some(), "should have some token: {token:?}");
1091 assert_eq!(
1092 access_boundary.as_deref(),
1093 Some("0x1234"),
1094 "should be 0x1234 but found: {access_boundary:?}"
1095 );
1096
1097 Ok(())
1098 }
1099}