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//! # async fn sample() -> anyhow::Result<()> {
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": "googleapis.com",
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(()) }
65//! ```
66//!
67//! [Best practices for using service accounts]: https://cloud.google.com/iam/docs/best-practices-service-accounts#choose-when-to-use
68//! [create a service account key]: https://cloud.google.com/iam/docs/keys-create-delete#creating
69//! [Service Account]: https://cloud.google.com/iam/docs/service-account-overview
70//! [service account key]: https://cloud.google.com/iam/docs/keys-create-delete#creating
71
72pub(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/// Represents the access specifier for a service account based token,
95/// specifying either OAuth 2.0 [scopes] or a [JWT] audience.
96///
97/// It ensures that only one of these access specifiers can be applied
98/// for a given credential setup.
99///
100/// [JWT]: https://google.aip.dev/auth/4111
101/// [scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
102#[derive(Clone, Debug, PartialEq)]
103#[allow(clippy::exhaustive_enums)]
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/// # async fn sample() -> anyhow::Result<()> {
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": "googleapis.com",
200/// });
201/// let credentials = Builder::new(key)
202///     .with_access_specifier(AccessSpecifier::from_audience("https://pubsub.googleapis.com"))
203///     .build()?;
204/// # Ok(()) }
205/// ```
206pub 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    /// Creates a new builder using [service_account_key] JSON value.
216    /// By default, the builder is configured with [cloud-platform] scope.
217    /// This can be overridden using the [with_access_specifier][Builder::with_access_specifier] method.
218    ///
219    /// [cloud-platform]:https://cloud.google.com/compute/docs/access/service-accounts#scopes_best_practice
220    /// [service_account_key]: https://cloud.google.com/iam/docs/keys-create-delete#creating
221    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    /// Sets the [AccessSpecifier] representing either scopes or audience for this credentials.
232    ///
233    /// # Example for setting audience
234    /// ```
235    /// # use google_cloud_auth::credentials::service_account::{AccessSpecifier, Builder};
236    /// let access_specifier = AccessSpecifier::from_audience("https://bigtable.googleapis.com/");
237    /// let service_account_key = serde_json::json!({ /* add details here */ });
238    /// let credentials = Builder::new(service_account_key)
239    ///     .with_access_specifier(access_specifier)
240    ///     .build();
241    /// ```
242    ///
243    /// # Example for setting scopes
244    /// ```
245    /// # use google_cloud_auth::credentials::service_account::{AccessSpecifier, Builder};
246    /// let access_specifier = AccessSpecifier::from_scopes(["https://www.googleapis.com/auth/pubsub"]);
247    /// let service_account_key = serde_json::json!({ /* add details here */ });
248    /// let credentials = Builder::new(service_account_key)
249    ///     .with_access_specifier(access_specifier)
250    ///     .build();
251    /// ```
252    pub fn with_access_specifier(mut self, access_specifier: AccessSpecifier) -> Self {
253        self.access_specifier = access_specifier;
254        self
255    }
256
257    /// Sets the [quota project] for this credentials.
258    ///
259    /// In some services, you can use a service account in
260    /// one project for authentication and authorization, and charge
261    /// the usage to a different project. This requires that the
262    /// service account has `serviceusage.services.use` permissions on the quota project.
263    ///
264    /// [quota project]: https://cloud.google.com/docs/quotas/quota-project
265    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    /// Sets the Google Cloud universe domain for these credentials.
271    ///
272    /// The universe domain is the default service domain for a given Cloud universe.
273    /// Any value provided here overrides a `universe_domain` value from the input service account JSON.
274    ///
275    /// # Example
276    /// ```
277    /// # use google_cloud_auth::credentials::service_account::Builder;
278    /// # use serde_json::json;
279    /// # async fn sample() -> anyhow::Result<()> {
280    /// # let config = json!({
281    /// #     "type": "service_account",
282    /// #     "client_email": "foo@bar.com",
283    /// #     "private_key": "---BEGIN---"
284    /// # });
285    /// let credentials = Builder::new(config)
286    ///     .with_universe_domain("googleapis.com")
287    ///     .build()?;
288    /// # Ok(()) }
289    /// ```
290    ///
291    /// [universe domain]: https://cloud.google.com/docs/authentication/universe-domain
292    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    /// Returns a [Credentials] instance with the configured settings.
315    ///
316    /// # Errors
317    ///
318    /// Returns an error if the `service_account_key`
319    /// provided to [`Builder::new`] cannot be successfully deserialized into the
320    /// expected format for a service account key. This typically happens if the
321    /// JSON value is malformed or missing required fields.
322    ///
323    /// For more information, on the expected format for a service account key,
324    /// consult the relevant section in the [service account keys] guide.
325    ///
326    /// [creating service account keys]: https://cloud.google.com/iam/docs/keys-create-delete#creating
327    pub fn build(self) -> BuildResult<Credentials> {
328        Ok(self.build_credentials()?.into())
329    }
330
331    /// Returns an [AccessTokenCredentials] instance with the configured settings.
332    ///
333    /// # Example
334    ///
335    /// ```
336    /// # use google_cloud_auth::credentials::service_account::Builder;
337    /// # use google_cloud_auth::credentials::{AccessTokenCredentials, AccessTokenCredentialsProvider};
338    /// # use serde_json::json;
339    /// # async fn sample() -> anyhow::Result<()> {
340    /// let service_account_key = json!({
341    ///     "client_email": "test-client-email",
342    ///     "private_key_id": "test-private-key-id",
343    ///     "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
344    ///     "project_id": "test-project-id",
345    ///     "universe_domain": "googleapis.com",
346    /// });
347    /// let credentials: AccessTokenCredentials = Builder::new(service_account_key)
348    ///     .with_quota_project_id("my-quota-project")
349    ///     .build_access_token_credentials()?;
350    /// let access_token = credentials.access_token().await?;
351    /// println!("Token: {}", access_token.token);
352    /// # Ok(()) }
353    /// ```
354    ///
355    /// # Errors
356    ///
357    /// Returns an error if the `service_account_key`
358    /// provided to [`Builder::new`] cannot be successfully deserialized into the
359    /// expected format for a service account key. This typically happens if the
360    /// JSON value is malformed or missing required fields.
361    ///
362    /// For more information, on the expected format for a service account key,
363    /// consult the relevant section in the [service account keys] guide.
364    ///
365    /// [service account keys]: https://cloud.google.com/iam/docs/keys-create-delete#creating
366    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    /// Returns a [crate::signer::Signer] instance with the configured settings.
397    ///
398    /// The returned [crate::signer::Signer] uses the service account's private key to sign blobs locally.
399    /// It does not make any network requests to perform signing operations.
400    ///
401    /// # Example
402    ///
403    /// ```
404    /// # use google_cloud_auth::credentials::service_account::Builder;
405    /// # use google_cloud_auth::signer::Signer;
406    /// # use serde_json::json;
407    /// # async fn sample() -> anyhow::Result<()> {
408    /// let service_account_key = json!({
409    ///     "client_email": "test-client-email",
410    ///     "private_key_id": "test-private-key-id",
411    ///     "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
412    ///     "project_id": "test-project-id",
413    /// });
414    ///
415    /// let signer: Signer = Builder::new(service_account_key).build_signer()?;
416    /// # Ok(()) }
417    /// ```
418    ///
419    /// # Errors
420    ///
421    /// Returns an error if the `service_account_key` provided to [`Builder::new`]
422    /// cannot be successfully deserialized or doesn't contain a valid private key.
423    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/// A representation of a [service account key].
436///
437/// [Service Account Key]: https://cloud.google.com/iam/docs/keys-create-delete#creating
438#[derive(serde::Deserialize, Default, Clone)]
439pub(crate) struct ServiceAccountKey {
440    /// The client email address of the service account.
441    /// (e.g., "my-sa@my-project.iam.gserviceaccount.com").
442    pub(crate) client_email: String,
443    /// ID of the service account's private key.
444    private_key_id: String,
445    /// The PEM-encoded PKCS#8 private key string associated with the service account.
446    /// Begins with `-----BEGIN PRIVATE KEY-----`.
447    private_key: String,
448    /// The project id the service account belongs to.
449    project_id: String,
450    /// The universe domain this service account belongs to.
451    pub(crate) universe_domain: Option<String>,
452}
453
454impl ServiceAccountKey {
455    // Creates a signer using the private key stored in the service account file.
456    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        // The claims encode a unix timestamp. `std::time::Instant` has no
577        // epoch, so we use `time::OffsetDateTime`, which reads system time, in
578        // the implementation.
579        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        // The issued at claim (`iat`) encodes a unix timestamp, in seconds.
878        // Advancing the clock by one second ensures that a subsequent claim has a
879        // different `iat`. Using `tokio::time::advance` changes `Instant::now()` without slowing down the test.
880        tokio::time::advance(Duration::from_secs(1)).await;
881
882        // Get the token again.
883        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        // Validate that the issued at claim is the same for the two tokens. If
890        // the 2nd token is not from the cache, its `iat` will be different.
891        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        // SA key without universe_domain
901        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        // SA key with universe_domain
914        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        // let the access boundary background thread update
1156        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}