Skip to main content

google_cloud_auth/credentials/
service_account.rs

1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! [Service Account] Credentials type.
16//!
17//! A service account is an account for an application or compute workload
18//! instead of an individual end user. The default credentials used by the
19//! client libraries may be, and often are, associated with a service account.
20//! Therefore, you can use service accounts by configuring your environment,
21//! without any code changes.
22//!
23//! Sometimes the application needs to use a [service account key] directly.
24//! The types in this module will help you in this case. For more information
25//! on when service account keys are appropriate, consult the
26//! relevant section in the [Best practices for using service accounts] guide.
27//!
28//! You can create multiple service account keys for a single service account.
29//! When you [create a service account key], the key is returned as a string.
30//! This string contains an ID for the service account, as well as the
31//! cryptographic materials (an RSA private key) required to authenticate the caller.
32//!
33//! Therefore, service account keys should be treated as any other secret
34//! with security implications. Think of them as unencrypted passwords. Do not
35//! store them where unauthorized persons or programs may read them.
36//!
37//! The types in this module allow you to create access tokens, based on
38//! service account keys and can be used with the Google Cloud client
39//! libraries for Rust.
40//!
41//! While the Google Cloud client libraries for Rust automatically use the types
42//! in this module when ADC finds a service account key file, you may want to
43//! use these types directly when the service account key is obtained from
44//! Cloud Secret Manager or a similar service.
45//!
46//! # Example
47//! ```
48//! # use google_cloud_auth::credentials::service_account::Builder;
49//! # use google_cloud_auth::credentials::Credentials;
50//! # use http::Extensions;
51//! # tokio_test::block_on(async {
52//! let service_account_key = serde_json::json!({
53//!     "client_email": "test-client-email",
54//!     "private_key_id": "test-private-key-id",
55//!     "private_key": "<YOUR_PKCS8_PEM_KEY_HERE>",
56//!     "project_id": "test-project-id",
57//!     "universe_domain": "test-universe-domain",
58//! });
59//! let credentials: Credentials = Builder::new(service_account_key)
60//!     .with_quota_project_id("my-quota-project")
61//!     .build()?;
62//! let headers = credentials.headers(Extensions::new()).await?;
63//! println!("Headers: {headers:?}");
64//! # Ok::<(), anyhow::Error>(())
65//! # });
66//! ```
67//!
68//! [Best practices for using service accounts]: https://cloud.google.com/iam/docs/best-practices-service-accounts#choose-when-to-use
69//! [create a service account key]: https://cloud.google.com/iam/docs/keys-create-delete#creating
70//! [Service Account]: https://cloud.google.com/iam/docs/service-account-overview
71//! [service account key]: https://cloud.google.com/iam/docs/keys-create-delete#creating
72
73pub(crate) mod jws;
74
75use crate::build_errors::Error as BuilderError;
76use crate::constants::DEFAULT_SCOPE;
77use crate::credentials::dynamic::{AccessTokenCredentialsProvider, CredentialsProvider};
78use crate::credentials::{AccessToken, AccessTokenCredentials, CacheableResource, Credentials};
79use crate::errors::{self};
80use crate::headers_util::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_pki_types::{PrivateKeyDer, pem::PemObject};
90use serde_json::Value;
91use std::sync::Arc;
92use time::OffsetDateTime;
93use tokio::time::Instant;
94
95/// Represents the access specifier for a service account based token,
96/// specifying either OAuth 2.0 [scopes] or a [JWT] audience.
97///
98/// It ensures that only one of these access specifiers can be applied
99/// for a given credential setup.
100///
101/// [JWT]: https://google.aip.dev/auth/4111
102/// [scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
103#[derive(Clone, Debug, PartialEq)]
104pub enum AccessSpecifier {
105    /// Use [AccessSpecifier::Audience] for setting audience in the token.
106    /// `aud` is a [JWT] claim specifying intended recipient of the token,
107    /// that is, a service.
108    /// Only one of audience or scopes can be specified for a credentials.
109    ///
110    /// [JWT]: https://google.aip.dev/auth/4111
111    Audience(String),
112
113    /// Use [AccessSpecifier::Scopes] for setting [scopes] in the token.
114    ///
115    /// `scopes` is a [JWT] claim specifying requested permission(s) for the token.
116    /// Only one of audience or scopes can be specified for a credentials.
117    ///
118    /// `scopes` define the *permissions being requested* for this specific session
119    /// when interacting with a service. For example, `https://www.googleapis.com/auth/devstorage.read_write`.
120    /// IAM permissions, on the other hand, define the *underlying capabilities*
121    /// the service account possesses within a system. For example, `storage.buckets.delete`.
122    /// When a token generated with specific scopes is used, the request must be permitted
123    /// by both the service account's underlying IAM permissions and the scopes requested
124    /// for the token. Therefore, scopes act as an additional restriction on what the token
125    /// can be used for. Please see relevant section in [service account authorization] to learn
126    /// more about scopes and IAM permissions.
127    ///
128    /// [JWT]: https://google.aip.dev/auth/4111
129    /// [service account authorization]: https://cloud.google.com/compute/docs/access/service-accounts#authorization
130    /// [scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
131    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    /// Creates [AccessSpecifier] with [scopes].
150    ///
151    /// # Example
152    /// ```
153    /// # use google_cloud_auth::credentials::service_account::{AccessSpecifier, Builder};
154    /// let access_specifier = AccessSpecifier::from_scopes(["https://www.googleapis.com/auth/pubsub"]);
155    /// let service_account_key = serde_json::json!({ /* add details here */ });
156    /// let credentials = Builder::new(service_account_key)
157    ///     .with_access_specifier(access_specifier)
158    ///     .build();
159    /// ```
160    ///
161    /// [scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
162    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    /// Creates [AccessSpecifier] with an audience.
171    ///
172    /// The value should be `https://{SERVICE}/`, e.g., `https://pubsub.googleapis.com/`
173    ///
174    /// # Example
175    /// ```
176    /// # use google_cloud_auth::credentials::service_account::{AccessSpecifier, Builder};
177    /// let access_specifier = AccessSpecifier::from_audience("https://bigtable.googleapis.com/");
178    /// let service_account_key = serde_json::json!({ /* add details here */ });
179    /// let credentials = Builder::new(service_account_key)
180    ///     .with_access_specifier(access_specifier)
181    ///     .build();
182    /// ```
183    pub fn from_audience<S: Into<String>>(audience: S) -> Self {
184        AccessSpecifier::Audience(audience.into())
185    }
186}
187
188/// A builder for constructing service account [Credentials] instances.
189///
190/// # Example
191/// ```
192/// # use google_cloud_auth::credentials::service_account::{AccessSpecifier, Builder};
193/// # tokio_test::block_on(async {
194/// let key = serde_json::json!({
195///     "client_email": "test-client-email",
196///     "private_key_id": "test-private-key-id",
197///     "private_key": "<YOUR_PKCS8_PEM_KEY_HERE>",
198///     "project_id": "test-project-id",
199///     "universe_domain": "test-universe-domain",
200/// });
201/// let credentials = Builder::new(key)
202///     .with_access_specifier(AccessSpecifier::from_audience("https://pubsub.googleapis.com"))
203///     .build();
204/// })
205/// ```
206pub struct Builder {
207    service_account_key: Value,
208    access_specifier: AccessSpecifier,
209    quota_project_id: Option<String>,
210}
211
212impl Builder {
213    /// Creates a new builder using [service_account_key] JSON value.
214    /// By default, the builder is configured with [cloud-platform] scope.
215    /// This can be overridden using the [with_access_specifier][Builder::with_access_specifier] method.
216    ///
217    /// [cloud-platform]:https://cloud.google.com/compute/docs/access/service-accounts#scopes_best_practice
218    /// [service_account_key]: https://cloud.google.com/iam/docs/keys-create-delete#creating
219    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    /// Sets the [AccessSpecifier] representing either scopes or audience for this credentials.
228    ///
229    /// # Example for setting audience
230    /// ```
231    /// # use google_cloud_auth::credentials::service_account::{AccessSpecifier, Builder};
232    /// let access_specifier = AccessSpecifier::from_audience("https://bigtable.googleapis.com/");
233    /// let service_account_key = serde_json::json!({ /* add details here */ });
234    /// let credentials = Builder::new(service_account_key)
235    ///     .with_access_specifier(access_specifier)
236    ///     .build();
237    /// ```
238    ///
239    /// # Example for setting scopes
240    /// ```
241    /// # use google_cloud_auth::credentials::service_account::{AccessSpecifier, Builder};
242    /// let access_specifier = AccessSpecifier::from_scopes(["https://www.googleapis.com/auth/pubsub"]);
243    /// let service_account_key = serde_json::json!({ /* add details here */ });
244    /// let credentials = Builder::new(service_account_key)
245    ///     .with_access_specifier(access_specifier)
246    ///     .build();
247    /// ```
248    pub fn with_access_specifier(mut self, access_specifier: AccessSpecifier) -> Self {
249        self.access_specifier = access_specifier;
250        self
251    }
252
253    /// Sets the [quota project] for this credentials.
254    ///
255    /// In some services, you can use a service account in
256    /// one project for authentication and authorization, and charge
257    /// the usage to a different project. This requires that the
258    /// service account has `serviceusage.services.use` permissions on the quota project.
259    ///
260    /// [quota project]: https://cloud.google.com/docs/quotas/quota-project
261    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    /// Returns a [Credentials] instance with the configured settings.
278    ///
279    /// # Errors
280    ///
281    /// Returns an error if the `service_account_key`
282    /// provided to [`Builder::new`] cannot be successfully deserialized into the
283    /// expected format for a service account key. This typically happens if the
284    /// JSON value is malformed or missing required fields.
285    ///
286    /// For more information, on the expected format for a service account key,
287    /// consult the relevant section in the [service account keys] guide.
288    ///
289    /// [creating service account keys]: https://cloud.google.com/iam/docs/keys-create-delete#creating
290    pub fn build(self) -> BuildResult<Credentials> {
291        Ok(self.build_access_token_credentials()?.into())
292    }
293
294    /// Returns an [AccessTokenCredentials] instance with the configured settings.
295    ///
296    /// # Example
297    ///
298    /// ```
299    /// # use google_cloud_auth::credentials::service_account::Builder;
300    /// # use google_cloud_auth::credentials::{AccessTokenCredentials, AccessTokenCredentialsProvider};
301    /// # use serde_json::json;
302    /// # tokio_test::block_on(async {
303    /// let service_account_key = json!({
304    ///     "client_email": "test-client-email",
305    ///     "private_key_id": "test-private-key-id",
306    ///     "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
307    ///     "project_id": "test-project-id",
308    ///     "universe_domain": "test-universe-domain",
309    /// });
310    /// let credentials: AccessTokenCredentials = Builder::new(service_account_key)
311    ///     .with_quota_project_id("my-quota-project")
312    ///     .build_access_token_credentials()?;
313    /// let access_token = credentials.access_token().await?;
314    /// println!("Token: {}", access_token.token);
315    /// # Ok::<(), anyhow::Error>(())
316    /// # });
317    /// ```
318    ///
319    /// # Errors
320    ///
321    /// Returns an error if the `service_account_key`
322    /// provided to [`Builder::new`] cannot be successfully deserialized into the
323    /// expected format for a service account key. This typically happens if the
324    /// JSON value is malformed or missing required fields.
325    ///
326    /// For more information, on the expected format for a service account key,
327    /// consult the relevant section in the [service account keys] guide.
328    ///
329    /// [service account keys]: https://cloud.google.com/iam/docs/keys-create-delete#creating
330    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    /// Returns a [crate::signer::Signer] instance with the configured settings.
340    ///
341    /// The returned [crate::signer::Signer] uses the service account's private key to sign blobs locally.
342    /// It does not make any network requests to perform signing operations.
343    ///
344    /// # Example
345    ///
346    /// ```
347    /// # use google_cloud_auth::credentials::service_account::Builder;
348    /// # use google_cloud_auth::signer::Signer;
349    /// # use serde_json::json;
350    /// # tokio_test::block_on(async {
351    /// let service_account_key = json!({
352    ///     "client_email": "test-client-email",
353    ///     "private_key_id": "test-private-key-id",
354    ///     "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
355    ///     "project_id": "test-project-id",
356    /// });
357    ///
358    /// let signer: Signer = Builder::new(service_account_key).build_signer()?;
359    /// # Ok::<(), anyhow::Error>(())
360    /// # });
361    /// ```
362    ///
363    /// # Errors
364    ///
365    /// Returns an error if the `service_account_key` provided to [`Builder::new`]
366    /// cannot be successfully deserialized or doesn't contain a valid private key.
367    pub fn build_signer(self) -> BuildResult<crate::signer::Signer> {
368        let service_account_key =
369            serde_json::from_value::<ServiceAccountKey>(self.service_account_key.clone())
370                .map_err(BuilderError::parsing)?;
371        let signing_provider =
372            crate::signer::service_account::ServiceAccountSigner::new(service_account_key);
373        Ok(crate::signer::Signer {
374            inner: Arc::new(signing_provider),
375        })
376    }
377}
378
379/// A representation of a [service account key].
380///
381/// [Service Account Key]: https://cloud.google.com/iam/docs/keys-create-delete#creating
382#[derive(serde::Deserialize, Default, Clone)]
383pub(crate) struct ServiceAccountKey {
384    /// The client email address of the service account.
385    /// (e.g., "my-sa@my-project.iam.gserviceaccount.com").
386    pub(crate) client_email: String,
387    /// ID of the service account's private key.
388    private_key_id: String,
389    /// The PEM-encoded PKCS#8 private key string associated with the service account.
390    /// Begins with `-----BEGIN PRIVATE KEY-----`.
391    private_key: String,
392    /// The project id the service account belongs to.
393    project_id: String,
394    /// The universe domain this service account belongs to.
395    universe_domain: Option<String>,
396}
397
398impl ServiceAccountKey {
399    // Creates a signer using the private key stored in the service account file.
400    pub(crate) fn signer(&self) -> Result<Box<dyn Signer>> {
401        let private_key = self.private_key.clone();
402        let key_provider = CryptoProvider::get_default().map(|p| p.key_provider);
403        #[cfg(feature = "default-rustls-provider")]
404        let key_provider = key_provider
405            .unwrap_or_else(|| rustls::crypto::aws_lc_rs::default_provider().key_provider);
406        #[cfg(not(feature = "default-rustls-provider"))]
407        let key_provider = key_provider.expect(
408            r###"
409The default rustls::CryptoProvider should be configured by the application. The
410`google-cloud-auth` crate was compiled without the `default-rustls-provider`
411feature. Without this feature the crate expects the application to initialize
412the rustls crypto provider using `rustls::CryptoProvider::install_default()`.
413
414Note that the application must use the exact same version of `rustls` as the
415`google-cloud-auth` crate does. Otherwise `install_default()` has no effect."###,
416        );
417
418        let key_der = PrivateKeyDer::from_pem_slice(private_key.as_bytes()).map_err(|e| {
419            errors::non_retryable_from_str(format!(
420                "Failed to parse service account private key PEM: {}",
421                e
422            ))
423        })?;
424
425        let pkcs8_der = match key_der {
426            PrivateKeyDer::Pkcs8(der) => der,
427            _ => {
428                return Err(errors::non_retryable_from_str(format!(
429                    "expected key to be in form of PKCS8, found {:?}",
430                    key_der
431                )));
432            }
433        };
434
435        let pk = key_provider
436            .load_private_key(PrivateKeyDer::Pkcs8(pkcs8_der))
437            .map_err(errors::non_retryable)?;
438
439        pk.choose_scheme(&[rustls::SignatureScheme::RSA_PKCS1_SHA256])
440            .ok_or_else(||{
441                errors::non_retryable_from_str("Unable to choose RSA_PKCS1_SHA256 signing scheme as it is not supported by current signer")
442            })
443    }
444}
445
446impl std::fmt::Debug for ServiceAccountKey {
447    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
448        f.debug_struct("ServiceAccountKey")
449            .field("client_email", &self.client_email)
450            .field("private_key_id", &self.private_key_id)
451            .field("private_key", &"[censored]")
452            .field("project_id", &self.project_id)
453            .field("universe_domain", &self.universe_domain)
454            .finish()
455    }
456}
457
458#[derive(Debug)]
459struct ServiceAccountCredentials<T>
460where
461    T: CachedTokenProvider,
462{
463    token_provider: T,
464    quota_project_id: Option<String>,
465}
466
467#[derive(Debug)]
468struct ServiceAccountTokenProvider {
469    service_account_key: ServiceAccountKey,
470    access_specifier: AccessSpecifier,
471}
472
473fn token_issue_time(current_time: OffsetDateTime) -> OffsetDateTime {
474    current_time - CLOCK_SKEW_FUDGE
475}
476
477fn token_expiry_time(current_time: OffsetDateTime) -> OffsetDateTime {
478    current_time + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT
479}
480
481#[async_trait]
482impl TokenProvider for ServiceAccountTokenProvider {
483    async fn token(&self) -> Result<Token> {
484        let expires_at = Instant::now() + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT;
485        let tg = ServiceAccountTokenGenerator {
486            audience: self.access_specifier.audience().cloned(),
487            scopes: self
488                .access_specifier
489                .scopes()
490                .map(|scopes| scopes.join(" ")),
491            service_account_key: self.service_account_key.clone(),
492            target_audience: None,
493        };
494
495        let token = tg.generate()?;
496
497        let token = Token {
498            token,
499            token_type: "Bearer".to_string(),
500            expires_at: Some(expires_at),
501            metadata: None,
502        };
503        Ok(token)
504    }
505}
506
507#[derive(Default, Clone)]
508pub(crate) struct ServiceAccountTokenGenerator {
509    service_account_key: ServiceAccountKey,
510    audience: Option<String>,
511    scopes: Option<String>,
512    target_audience: Option<String>,
513}
514
515impl ServiceAccountTokenGenerator {
516    #[cfg(feature = "idtoken")]
517    pub(crate) fn new_id_token_generator(
518        target_audience: String,
519        audience: String,
520        service_account_key: ServiceAccountKey,
521    ) -> Self {
522        Self {
523            service_account_key,
524            target_audience: Some(target_audience),
525            audience: Some(audience),
526            scopes: None,
527        }
528    }
529
530    pub(crate) fn generate(&self) -> Result<String> {
531        let signer = self.service_account_key.signer()?;
532
533        // The claims encode a unix timestamp. `std::time::Instant` has no
534        // epoch, so we use `time::OffsetDateTime`, which reads system time, in
535        // the implementation.
536        let current_time = OffsetDateTime::now_utc();
537
538        let claims = JwsClaims {
539            iss: self.service_account_key.client_email.clone(),
540            scope: self.scopes.clone(),
541            target_audience: self.target_audience.clone(),
542            aud: self.audience.clone(),
543            exp: token_expiry_time(current_time),
544            iat: token_issue_time(current_time),
545            typ: None,
546            sub: Some(self.service_account_key.client_email.clone()),
547        };
548
549        let header = JwsHeader {
550            alg: "RS256",
551            typ: "JWT",
552            kid: Some(self.service_account_key.private_key_id.clone()),
553        };
554        let encoded_header_claims = format!("{}.{}", header.encode()?, claims.encode()?);
555        let sig = signer
556            .sign(encoded_header_claims.as_bytes())
557            .map_err(errors::non_retryable)?;
558        use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine as _};
559        let token = format!(
560            "{}.{}",
561            encoded_header_claims,
562            &BASE64_URL_SAFE_NO_PAD.encode(sig)
563        );
564
565        Ok(token)
566    }
567}
568
569#[async_trait::async_trait]
570impl<T> CredentialsProvider for ServiceAccountCredentials<T>
571where
572    T: CachedTokenProvider,
573{
574    async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
575        let token = self.token_provider.token(extensions).await?;
576        build_cacheable_headers(&token, &self.quota_project_id)
577    }
578}
579
580#[async_trait::async_trait]
581impl<T> AccessTokenCredentialsProvider for ServiceAccountCredentials<T>
582where
583    T: CachedTokenProvider,
584{
585    async fn access_token(&self) -> Result<AccessToken> {
586        let token = self.token_provider.token(Extensions::new()).await?;
587        token.into()
588    }
589}
590
591#[cfg(test)]
592mod tests {
593    use super::*;
594    use crate::credentials::QUOTA_PROJECT_KEY;
595    use crate::credentials::tests::{
596        PKCS8_PK, b64_decode_to_json, get_headers_from_cache, get_token_from_headers,
597    };
598    use crate::token::tests::MockTokenProvider;
599    use http::HeaderValue;
600    use http::header::AUTHORIZATION;
601    use rsa::pkcs1::EncodeRsaPrivateKey;
602    use rsa::pkcs8::LineEnding;
603    use serde_json::Value;
604    use serde_json::json;
605    use std::error::Error as _;
606    use std::time::Duration;
607
608    type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
609
610    const SSJ_REGEX: &str = r"(?<header>[^\.]+)\.(?<claims>[^\.]+)\.(?<sig>[^\.]+)";
611
612    #[test]
613    fn debug_token_provider() {
614        let expected = ServiceAccountKey {
615            client_email: "test-client-email".to_string(),
616            private_key_id: "test-private-key-id".to_string(),
617            private_key: "super-duper-secret-private-key".to_string(),
618            project_id: "test-project-id".to_string(),
619            universe_domain: Some("test-universe-domain".to_string()),
620        };
621        let fmt = format!("{expected:?}");
622        assert!(fmt.contains("test-client-email"), "{fmt}");
623        assert!(fmt.contains("test-private-key-id"), "{fmt}");
624        assert!(!fmt.contains("super-duper-secret-private-key"), "{fmt}");
625        assert!(fmt.contains("test-project-id"), "{fmt}");
626        assert!(fmt.contains("test-universe-domain"), "{fmt}");
627    }
628
629    #[test]
630    fn validate_token_issue_time() {
631        let current_time = OffsetDateTime::now_utc();
632        let token_issue_time = token_issue_time(current_time);
633        assert!(token_issue_time == current_time - CLOCK_SKEW_FUDGE);
634    }
635
636    #[test]
637    fn validate_token_expiry_time() {
638        let current_time = OffsetDateTime::now_utc();
639        let token_issue_time = token_expiry_time(current_time);
640        assert!(token_issue_time == current_time + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT);
641    }
642
643    #[tokio::test]
644    async fn headers_success_without_quota_project() -> TestResult {
645        let token = Token {
646            token: "test-token".to_string(),
647            token_type: "Bearer".to_string(),
648            expires_at: None,
649            metadata: None,
650        };
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: None,
658        };
659
660        let mut extensions = Extensions::new();
661        let cached_headers = sac.headers(extensions.clone()).await.unwrap();
662        let (headers, entity_tag) = match cached_headers {
663            CacheableResource::New { entity_tag, data } => (data, entity_tag),
664            CacheableResource::NotModified => unreachable!("expecting new headers"),
665        };
666        let token = headers.get(AUTHORIZATION).unwrap();
667
668        assert_eq!(headers.len(), 1, "{headers:?}");
669        assert_eq!(token, HeaderValue::from_static("Bearer test-token"));
670        assert!(token.is_sensitive());
671
672        extensions.insert(entity_tag);
673
674        let cached_headers = sac.headers(extensions).await?;
675
676        match cached_headers {
677            CacheableResource::New { .. } => unreachable!("expecting new headers"),
678            CacheableResource::NotModified => CacheableResource::<HeaderMap>::NotModified,
679        };
680        Ok(())
681    }
682
683    #[tokio::test]
684    async fn headers_success_with_quota_project() -> TestResult {
685        let token = Token {
686            token: "test-token".to_string(),
687            token_type: "Bearer".to_string(),
688            expires_at: None,
689            metadata: None,
690        };
691
692        let quota_project = "test-quota-project";
693
694        let mut mock = MockTokenProvider::new();
695        mock.expect_token().times(1).return_once(|| Ok(token));
696
697        let sac = ServiceAccountCredentials {
698            token_provider: TokenCache::new(mock),
699            quota_project_id: Some(quota_project.to_string()),
700        };
701
702        let headers = get_headers_from_cache(sac.headers(Extensions::new()).await.unwrap())?;
703        let token = headers.get(AUTHORIZATION).unwrap();
704        let quota_project_header = headers.get(QUOTA_PROJECT_KEY).unwrap();
705
706        assert_eq!(headers.len(), 2, "{headers:?}");
707        assert_eq!(token, HeaderValue::from_static("Bearer test-token"));
708        assert!(token.is_sensitive());
709        assert_eq!(
710            quota_project_header,
711            HeaderValue::from_static(quota_project)
712        );
713        assert!(!quota_project_header.is_sensitive());
714        Ok(())
715    }
716
717    #[tokio::test]
718    async fn headers_failure() {
719        let mut mock = MockTokenProvider::new();
720        mock.expect_token()
721            .times(1)
722            .return_once(|| Err(errors::non_retryable_from_str("fail")));
723
724        let sac = ServiceAccountCredentials {
725            token_provider: TokenCache::new(mock),
726            quota_project_id: None,
727        };
728        assert!(sac.headers(Extensions::new()).await.is_err());
729    }
730
731    fn get_mock_service_key() -> Value {
732        json!({
733            "client_email": "test-client-email",
734            "private_key_id": "test-private-key-id",
735            "private_key": "",
736            "project_id": "test-project-id",
737        })
738    }
739
740    #[tokio::test]
741    async fn get_service_account_headers_pkcs1_private_key_failure() -> TestResult {
742        let mut service_account_key = get_mock_service_key();
743
744        let key = crate::credentials::tests::RSA_PRIVATE_KEY
745            .to_pkcs1_pem(LineEnding::LF)
746            .expect("Failed to encode key to PKCS#1 PEM")
747            .to_string();
748
749        service_account_key["private_key"] = Value::from(key);
750        let cred = Builder::new(service_account_key).build()?;
751        let expected_error_message = "expected key to be in form of PKCS8, found ";
752        assert!(
753            cred.headers(Extensions::new())
754                .await
755                .is_err_and(|e| e.to_string().contains(expected_error_message))
756        );
757        Ok(())
758    }
759
760    #[tokio::test]
761    async fn get_service_account_token_pkcs8_key_success() -> TestResult {
762        let mut service_account_key = get_mock_service_key();
763        service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
764        let tp = Builder::new(service_account_key.clone()).build_token_provider()?;
765
766        let token = tp.token().await?;
767        let re = regex::Regex::new(SSJ_REGEX).unwrap();
768        let captures = re.captures(&token.token).ok_or_else(|| {
769            format!(
770                r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {}"#,
771                token.token
772            )
773        })?;
774        let header = b64_decode_to_json(captures["header"].to_string());
775        assert_eq!(header["alg"], "RS256");
776        assert_eq!(header["typ"], "JWT");
777        assert_eq!(header["kid"], service_account_key["private_key_id"]);
778
779        let claims = b64_decode_to_json(captures["claims"].to_string());
780        assert_eq!(claims["iss"], service_account_key["client_email"]);
781        assert_eq!(claims["scope"], DEFAULT_SCOPE);
782        assert!(claims["iat"].is_number());
783        assert!(claims["exp"].is_number());
784        assert_eq!(claims["sub"], service_account_key["client_email"]);
785
786        Ok(())
787    }
788
789    #[tokio::test]
790    async fn header_caching() -> TestResult {
791        let private_key = PKCS8_PK.clone();
792
793        let json_value = json!({
794            "client_email": "test-client-email",
795            "private_key_id": "test-private-key-id",
796            "private_key": private_key,
797            "project_id": "test-project-id",
798            "universe_domain": "test-universe-domain"
799        });
800
801        let credentials = Builder::new(json_value).build()?;
802
803        let headers = credentials.headers(Extensions::new()).await?;
804
805        let re = regex::Regex::new(SSJ_REGEX).unwrap();
806        let token = get_token_from_headers(headers).unwrap();
807
808        let captures = re.captures(&token).unwrap();
809
810        let claims = b64_decode_to_json(captures["claims"].to_string());
811        let first_iat = claims["iat"].as_i64().unwrap();
812
813        // The issued at claim (`iat`) encodes a unix timestamp, in seconds.
814        // Sleeping for one second ensures that a subsequent claim has a
815        // different `iat`. We need a real sleep, because we cannot fake the
816        // current unix timestamp.
817        std::thread::sleep(Duration::from_secs(1));
818
819        // Get the token again.
820        let token = get_token_from_headers(credentials.headers(Extensions::new()).await?).unwrap();
821        let captures = re.captures(&token).unwrap();
822
823        let claims = b64_decode_to_json(captures["claims"].to_string());
824        let second_iat = claims["iat"].as_i64().unwrap();
825
826        // Validate that the issued at claim is the same for the two tokens. If
827        // the 2nd token is not from the cache, its `iat` will be different.
828        assert_eq!(first_iat, second_iat);
829
830        Ok(())
831    }
832
833    #[tokio::test]
834    async fn get_service_account_headers_invalid_key_failure() -> TestResult {
835        let mut service_account_key = get_mock_service_key();
836        let pem_data = "-----BEGIN PRIVATE KEY-----\nMIGkAg==\n-----END PRIVATE KEY-----";
837        service_account_key["private_key"] = Value::from(pem_data);
838        let cred = Builder::new(service_account_key).build()?;
839
840        let token = cred.headers(Extensions::new()).await;
841        let err = token.unwrap_err();
842        assert!(!err.is_transient(), "{err:?}");
843        let source = err.source().and_then(|e| e.downcast_ref::<rustls::Error>());
844        assert!(matches!(source, Some(rustls::Error::General(_))), "{err:?}");
845        Ok(())
846    }
847
848    #[tokio::test]
849    async fn get_service_account_invalid_json_failure() -> TestResult {
850        let service_account_key = Value::from(" ");
851        let e = Builder::new(service_account_key).build().unwrap_err();
852        assert!(e.is_parsing(), "{e:?}");
853
854        Ok(())
855    }
856
857    #[test]
858    fn signer_failure() -> TestResult {
859        let tp = Builder::new(get_mock_service_key()).build_token_provider()?;
860        let tg = ServiceAccountTokenGenerator {
861            service_account_key: tp.service_account_key.clone(),
862            ..Default::default()
863        };
864
865        let signer = tg.service_account_key.signer();
866        let expected_error_message = "Failed to parse service account private key PEM";
867        assert!(signer.is_err_and(|e| e.to_string().contains(expected_error_message)));
868        Ok(())
869    }
870
871    #[test]
872    fn signer_fails_on_invalid_pem_type() -> TestResult {
873        let invalid_pem = concat!(
874            "-----BEGI X509 CRL-----\n",
875            "MIIBmzCBja... (truncated) ...\n",
876            "-----END X509 CRL-----"
877        );
878
879        let mut key = ServiceAccountKey {
880            private_key: invalid_pem.to_string(),
881            ..Default::default()
882        };
883        key.private_key = invalid_pem.to_string();
884        let result = key.signer();
885        assert!(result.is_err());
886        let error_msg = result.unwrap_err().to_string();
887        assert!(error_msg.contains("Failed to parse service account private key PEM"));
888        Ok(())
889    }
890
891    #[tokio::test]
892    async fn get_service_account_headers_with_audience() -> TestResult {
893        let mut service_account_key = get_mock_service_key();
894        service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
895        let headers = Builder::new(service_account_key.clone())
896            .with_access_specifier(AccessSpecifier::from_audience("test-audience"))
897            .build()?
898            .headers(Extensions::new())
899            .await?;
900
901        let re = regex::Regex::new(SSJ_REGEX).unwrap();
902        let token = get_token_from_headers(headers).unwrap();
903        let captures = re.captures(&token).ok_or_else(|| {
904            format!(r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {token}"#)
905        })?;
906        let token_header = b64_decode_to_json(captures["header"].to_string());
907        assert_eq!(token_header["alg"], "RS256");
908        assert_eq!(token_header["typ"], "JWT");
909        assert_eq!(token_header["kid"], service_account_key["private_key_id"]);
910
911        let claims = b64_decode_to_json(captures["claims"].to_string());
912        assert_eq!(claims["iss"], service_account_key["client_email"]);
913        assert_eq!(claims["scope"], Value::Null);
914        assert_eq!(claims["aud"], "test-audience");
915        assert!(claims["iat"].is_number());
916        assert!(claims["exp"].is_number());
917        assert_eq!(claims["sub"], service_account_key["client_email"]);
918        Ok(())
919    }
920
921    #[tokio::test(start_paused = true)]
922    async fn get_service_account_token_verify_expiry_time() -> TestResult {
923        let now = Instant::now();
924        let mut service_account_key = get_mock_service_key();
925        service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
926        let token = Builder::new(service_account_key)
927            .build_token_provider()?
928            .token()
929            .await?;
930
931        let expected_expiry = now + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT;
932
933        assert_eq!(token.expires_at.unwrap(), expected_expiry);
934        Ok(())
935    }
936
937    #[tokio::test]
938    async fn get_service_account_headers_with_custom_scopes() -> TestResult {
939        let mut service_account_key = get_mock_service_key();
940        let scopes = vec![
941            "https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate",
942        ];
943        service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
944        let headers = Builder::new(service_account_key.clone())
945            .with_access_specifier(AccessSpecifier::from_scopes(scopes.clone()))
946            .build()?
947            .headers(Extensions::new())
948            .await?;
949
950        let re = regex::Regex::new(SSJ_REGEX).unwrap();
951        let token = get_token_from_headers(headers).unwrap();
952        let captures = re.captures(&token).ok_or_else(|| {
953            format!(r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {token}"#)
954        })?;
955        let token_header = b64_decode_to_json(captures["header"].to_string());
956        assert_eq!(token_header["alg"], "RS256");
957        assert_eq!(token_header["typ"], "JWT");
958        assert_eq!(token_header["kid"], service_account_key["private_key_id"]);
959
960        let claims = b64_decode_to_json(captures["claims"].to_string());
961        assert_eq!(claims["iss"], service_account_key["client_email"]);
962        assert_eq!(claims["scope"], scopes.join(" "));
963        assert_eq!(claims["aud"], Value::Null);
964        assert!(claims["iat"].is_number());
965        assert!(claims["exp"].is_number());
966        assert_eq!(claims["sub"], service_account_key["client_email"]);
967        Ok(())
968    }
969
970    #[tokio::test]
971    async fn get_service_account_access_token() -> TestResult {
972        let mut service_account_key = get_mock_service_key();
973        service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
974        let creds = Builder::new(service_account_key.clone()).build_access_token_credentials()?;
975
976        let access_token = creds.access_token().await?;
977        let token = access_token.token;
978
979        let re = regex::Regex::new(SSJ_REGEX).unwrap();
980        let captures = re.captures(&token).ok_or_else(|| {
981            format!(r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {token}"#)
982        })?;
983        let token_header = b64_decode_to_json(captures["header"].to_string());
984        assert_eq!(token_header["alg"], "RS256");
985        assert_eq!(token_header["typ"], "JWT");
986        assert_eq!(token_header["kid"], service_account_key["private_key_id"]);
987
988        Ok(())
989    }
990
991    #[tokio::test]
992    async fn get_service_account_signer() -> TestResult {
993        let mut service_account_key = get_mock_service_key();
994        service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
995        let signer = Builder::new(service_account_key.clone()).build_signer()?;
996
997        let client_email = signer.client_email().await?;
998        assert_eq!(client_email, service_account_key["client_email"]);
999
1000        let result = signer.sign(b"test").await;
1001
1002        assert!(result.is_ok());
1003
1004        Ok(())
1005    }
1006}