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)]
103pub enum AccessSpecifier {
104    /// Use [AccessSpecifier::Audience] for setting audience in the token.
105    /// `aud` is a [JWT] claim specifying intended recipient of the token,
106    /// that is, a service.
107    /// Only one of audience or scopes can be specified for a credentials.
108    ///
109    /// [JWT]: https://google.aip.dev/auth/4111
110    Audience(String),
111
112    /// Use [AccessSpecifier::Scopes] for setting [scopes] in the token.
113    ///
114    /// `scopes` is a [JWT] claim specifying requested permission(s) for the token.
115    /// Only one of audience or scopes can be specified for a credentials.
116    ///
117    /// `scopes` define the *permissions being requested* for this specific session
118    /// when interacting with a service. For example, `https://www.googleapis.com/auth/devstorage.read_write`.
119    /// IAM permissions, on the other hand, define the *underlying capabilities*
120    /// the service account possesses within a system. For example, `storage.buckets.delete`.
121    /// When a token generated with specific scopes is used, the request must be permitted
122    /// by both the service account's underlying IAM permissions and the scopes requested
123    /// for the token. Therefore, scopes act as an additional restriction on what the token
124    /// can be used for. Please see relevant section in [service account authorization] to learn
125    /// more about scopes and IAM permissions.
126    ///
127    /// [JWT]: https://google.aip.dev/auth/4111
128    /// [service account authorization]: https://cloud.google.com/compute/docs/access/service-accounts#authorization
129    /// [scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
130    Scopes(Vec<String>),
131}
132
133impl AccessSpecifier {
134    fn audience(&self) -> Option<&String> {
135        match self {
136            AccessSpecifier::Audience(aud) => Some(aud),
137            AccessSpecifier::Scopes(_) => None,
138        }
139    }
140
141    fn scopes(&self) -> Option<&[String]> {
142        match self {
143            AccessSpecifier::Scopes(scopes) => Some(scopes),
144            AccessSpecifier::Audience(_) => None,
145        }
146    }
147
148    /// Creates [AccessSpecifier] with [scopes].
149    ///
150    /// # Example
151    /// ```
152    /// # use google_cloud_auth::credentials::service_account::{AccessSpecifier, Builder};
153    /// let access_specifier = AccessSpecifier::from_scopes(["https://www.googleapis.com/auth/pubsub"]);
154    /// let service_account_key = serde_json::json!({ /* add details here */ });
155    /// let credentials = Builder::new(service_account_key)
156    ///     .with_access_specifier(access_specifier)
157    ///     .build();
158    /// ```
159    ///
160    /// [scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
161    pub fn from_scopes<I, S>(scopes: I) -> Self
162    where
163        I: IntoIterator<Item = S>,
164        S: Into<String>,
165    {
166        AccessSpecifier::Scopes(scopes.into_iter().map(|s| s.into()).collect())
167    }
168
169    /// Creates [AccessSpecifier] with an audience.
170    ///
171    /// The value should be `https://{SERVICE}/`, e.g., `https://pubsub.googleapis.com/`
172    ///
173    /// # Example
174    /// ```
175    /// # use google_cloud_auth::credentials::service_account::{AccessSpecifier, Builder};
176    /// let access_specifier = AccessSpecifier::from_audience("https://bigtable.googleapis.com/");
177    /// let service_account_key = serde_json::json!({ /* add details here */ });
178    /// let credentials = Builder::new(service_account_key)
179    ///     .with_access_specifier(access_specifier)
180    ///     .build();
181    /// ```
182    pub fn from_audience<S: Into<String>>(audience: S) -> Self {
183        AccessSpecifier::Audience(audience.into())
184    }
185}
186
187/// A builder for constructing service account [Credentials] instances.
188///
189/// # Example
190/// ```
191/// # use google_cloud_auth::credentials::service_account::{AccessSpecifier, Builder};
192/// # async fn sample() -> anyhow::Result<()> {
193/// let key = serde_json::json!({
194///     "client_email": "test-client-email",
195///     "private_key_id": "test-private-key-id",
196///     "private_key": "<YOUR_PKCS8_PEM_KEY_HERE>",
197///     "project_id": "test-project-id",
198///     "universe_domain": "googleapis.com",
199/// });
200/// let credentials = Builder::new(key)
201///     .with_access_specifier(AccessSpecifier::from_audience("https://pubsub.googleapis.com"))
202///     .build()?;
203/// # Ok(()) }
204/// ```
205pub struct Builder {
206    service_account_key: Value,
207    access_specifier: AccessSpecifier,
208    quota_project_id: Option<String>,
209    universe_domain: Option<String>,
210    iam_endpoint_override: Option<String>,
211}
212
213impl Builder {
214    /// Creates a new builder using [service_account_key] JSON value.
215    /// By default, the builder is configured with [cloud-platform] scope.
216    /// This can be overridden using the [with_access_specifier][Builder::with_access_specifier] method.
217    ///
218    /// [cloud-platform]:https://cloud.google.com/compute/docs/access/service-accounts#scopes_best_practice
219    /// [service_account_key]: https://cloud.google.com/iam/docs/keys-create-delete#creating
220    pub fn new(service_account_key: Value) -> Self {
221        Self {
222            service_account_key,
223            access_specifier: AccessSpecifier::Scopes([DEFAULT_SCOPE].map(str::to_string).to_vec()),
224            quota_project_id: None,
225            universe_domain: None,
226            iam_endpoint_override: None,
227        }
228    }
229
230    /// Sets the [AccessSpecifier] representing either scopes or audience for this credentials.
231    ///
232    /// # Example for setting audience
233    /// ```
234    /// # use google_cloud_auth::credentials::service_account::{AccessSpecifier, Builder};
235    /// let access_specifier = AccessSpecifier::from_audience("https://bigtable.googleapis.com/");
236    /// let service_account_key = serde_json::json!({ /* add details here */ });
237    /// let credentials = Builder::new(service_account_key)
238    ///     .with_access_specifier(access_specifier)
239    ///     .build();
240    /// ```
241    ///
242    /// # Example for setting scopes
243    /// ```
244    /// # use google_cloud_auth::credentials::service_account::{AccessSpecifier, Builder};
245    /// let access_specifier = AccessSpecifier::from_scopes(["https://www.googleapis.com/auth/pubsub"]);
246    /// let service_account_key = serde_json::json!({ /* add details here */ });
247    /// let credentials = Builder::new(service_account_key)
248    ///     .with_access_specifier(access_specifier)
249    ///     .build();
250    /// ```
251    pub fn with_access_specifier(mut self, access_specifier: AccessSpecifier) -> Self {
252        self.access_specifier = access_specifier;
253        self
254    }
255
256    /// Sets the [quota project] for this credentials.
257    ///
258    /// In some services, you can use a service account in
259    /// one project for authentication and authorization, and charge
260    /// the usage to a different project. This requires that the
261    /// service account has `serviceusage.services.use` permissions on the quota project.
262    ///
263    /// [quota project]: https://cloud.google.com/docs/quotas/quota-project
264    pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
265        self.quota_project_id = Some(quota_project_id.into());
266        self
267    }
268
269    /// Sets the Google Cloud universe domain for these credentials.
270    ///
271    /// The universe domain is the default service domain for a given Cloud universe.
272    /// Any value provided here overrides a `universe_domain` value from the input service account JSON.
273    ///
274    /// # Example
275    /// ```
276    /// # use google_cloud_auth::credentials::service_account::Builder;
277    /// # use serde_json::json;
278    /// # async fn sample() -> anyhow::Result<()> {
279    /// # let config = json!({
280    /// #     "type": "service_account",
281    /// #     "client_email": "foo@bar.com",
282    /// #     "private_key": "---BEGIN---"
283    /// # });
284    /// let credentials = Builder::new(config)
285    ///     .with_universe_domain("googleapis.com")
286    ///     .build()?;
287    /// # Ok(()) }
288    /// ```
289    ///
290    /// [universe domain]: https://cloud.google.com/docs/authentication/universe-domain
291    pub fn with_universe_domain<S: Into<String>>(mut self, universe_domain: S) -> Self {
292        self.universe_domain = Some(universe_domain.into());
293        self
294    }
295
296    #[cfg(all(test, google_cloud_unstable_trusted_boundaries))]
297    fn maybe_iam_endpoint_override(mut self, iam_endpoint_override: Option<String>) -> Self {
298        self.iam_endpoint_override = iam_endpoint_override;
299        self
300    }
301
302    fn build_token_provider(self) -> BuildResult<ServiceAccountTokenProvider> {
303        let service_account_key =
304            serde_json::from_value::<ServiceAccountKey>(self.service_account_key)
305                .map_err(BuilderError::parsing)?;
306
307        Ok(ServiceAccountTokenProvider {
308            service_account_key,
309            access_specifier: self.access_specifier,
310        })
311    }
312
313    /// Returns a [Credentials] instance with the configured settings.
314    ///
315    /// # Errors
316    ///
317    /// Returns an error if the `service_account_key`
318    /// provided to [`Builder::new`] cannot be successfully deserialized into the
319    /// expected format for a service account key. This typically happens if the
320    /// JSON value is malformed or missing required fields.
321    ///
322    /// For more information, on the expected format for a service account key,
323    /// consult the relevant section in the [service account keys] guide.
324    ///
325    /// [creating service account keys]: https://cloud.google.com/iam/docs/keys-create-delete#creating
326    pub fn build(self) -> BuildResult<Credentials> {
327        Ok(self.build_credentials()?.into())
328    }
329
330    /// Returns an [AccessTokenCredentials] instance with the configured settings.
331    ///
332    /// # Example
333    ///
334    /// ```
335    /// # use google_cloud_auth::credentials::service_account::Builder;
336    /// # use google_cloud_auth::credentials::{AccessTokenCredentials, AccessTokenCredentialsProvider};
337    /// # use serde_json::json;
338    /// # async fn sample() -> anyhow::Result<()> {
339    /// let service_account_key = json!({
340    ///     "client_email": "test-client-email",
341    ///     "private_key_id": "test-private-key-id",
342    ///     "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
343    ///     "project_id": "test-project-id",
344    ///     "universe_domain": "googleapis.com",
345    /// });
346    /// let credentials: AccessTokenCredentials = Builder::new(service_account_key)
347    ///     .with_quota_project_id("my-quota-project")
348    ///     .build_access_token_credentials()?;
349    /// let access_token = credentials.access_token().await?;
350    /// println!("Token: {}", access_token.token);
351    /// # Ok(()) }
352    /// ```
353    ///
354    /// # Errors
355    ///
356    /// Returns an error if the `service_account_key`
357    /// provided to [`Builder::new`] cannot be successfully deserialized into the
358    /// expected format for a service account key. This typically happens if the
359    /// JSON value is malformed or missing required fields.
360    ///
361    /// For more information, on the expected format for a service account key,
362    /// consult the relevant section in the [service account keys] guide.
363    ///
364    /// [service account keys]: https://cloud.google.com/iam/docs/keys-create-delete#creating
365    pub fn build_access_token_credentials(self) -> BuildResult<AccessTokenCredentials> {
366        Ok(self.build_credentials()?.into())
367    }
368
369    fn build_credentials(
370        self,
371    ) -> BuildResult<CredentialsWithAccessBoundary<ServiceAccountCredentials<TokenCache>>> {
372        let iam_endpoint = self.iam_endpoint_override.clone();
373        let quota_project_id = self.quota_project_id.clone();
374        let universe_domain_override = self.universe_domain.clone();
375        let token_provider = self.build_token_provider()?;
376        let client_email = token_provider.service_account_key.client_email.clone();
377        let universe_domain =
378            universe_domain_override.or(token_provider.service_account_key.universe_domain.clone());
379        let access_boundary_url = crate::access_boundary::service_account_lookup_url(
380            &client_email,
381            iam_endpoint.as_deref(),
382        );
383        let creds = ServiceAccountCredentials {
384            quota_project_id,
385            token_provider: TokenCache::new(token_provider),
386            universe_domain,
387        };
388
389        Ok(CredentialsWithAccessBoundary::new(
390            creds,
391            Some(access_boundary_url),
392        ))
393    }
394
395    /// Returns a [crate::signer::Signer] instance with the configured settings.
396    ///
397    /// The returned [crate::signer::Signer] uses the service account's private key to sign blobs locally.
398    /// It does not make any network requests to perform signing operations.
399    ///
400    /// # Example
401    ///
402    /// ```
403    /// # use google_cloud_auth::credentials::service_account::Builder;
404    /// # use google_cloud_auth::signer::Signer;
405    /// # use serde_json::json;
406    /// # async fn sample() -> anyhow::Result<()> {
407    /// let service_account_key = json!({
408    ///     "client_email": "test-client-email",
409    ///     "private_key_id": "test-private-key-id",
410    ///     "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
411    ///     "project_id": "test-project-id",
412    /// });
413    ///
414    /// let signer: Signer = Builder::new(service_account_key).build_signer()?;
415    /// # Ok(()) }
416    /// ```
417    ///
418    /// # Errors
419    ///
420    /// Returns an error if the `service_account_key` provided to [`Builder::new`]
421    /// cannot be successfully deserialized or doesn't contain a valid private key.
422    pub fn build_signer(self) -> BuildResult<crate::signer::Signer> {
423        let service_account_key =
424            serde_json::from_value::<ServiceAccountKey>(self.service_account_key.clone())
425                .map_err(BuilderError::parsing)?;
426        let signing_provider =
427            crate::signer::service_account::ServiceAccountSigner::new(service_account_key);
428        Ok(crate::signer::Signer {
429            inner: Arc::new(signing_provider),
430        })
431    }
432}
433
434/// A representation of a [service account key].
435///
436/// [Service Account Key]: https://cloud.google.com/iam/docs/keys-create-delete#creating
437#[derive(serde::Deserialize, Default, Clone)]
438pub(crate) struct ServiceAccountKey {
439    /// The client email address of the service account.
440    /// (e.g., "my-sa@my-project.iam.gserviceaccount.com").
441    pub(crate) client_email: String,
442    /// ID of the service account's private key.
443    private_key_id: String,
444    /// The PEM-encoded PKCS#8 private key string associated with the service account.
445    /// Begins with `-----BEGIN PRIVATE KEY-----`.
446    private_key: String,
447    /// The project id the service account belongs to.
448    project_id: String,
449    /// The universe domain this service account belongs to.
450    pub(crate) universe_domain: Option<String>,
451}
452
453impl ServiceAccountKey {
454    // Creates a signer using the private key stored in the service account file.
455    pub(crate) fn signer(&self) -> Result<Box<dyn Signer>> {
456        let private_key = self.private_key.clone();
457        let key_provider = crate::credentials::crypto_provider::get_key_provider();
458
459        let key_der = PrivateKeyDer::from_pem_slice(private_key.as_bytes()).map_err(|e| {
460            errors::non_retryable_from_str(format!(
461                "failed to parse service account private key PEM: {}",
462                e
463            ))
464        })?;
465
466        let pkcs8_der = match key_der {
467            PrivateKeyDer::Pkcs8(der) => der,
468            _ => {
469                return Err(errors::non_retryable_from_str(format!(
470                    "expected key to be in form of PKCS8, found {:?}",
471                    key_der
472                )));
473            }
474        };
475
476        let pk = key_provider
477            .load_private_key(PrivateKeyDer::Pkcs8(pkcs8_der))
478            .map_err(errors::non_retryable)?;
479
480        pk.choose_scheme(&[rustls::SignatureScheme::RSA_PKCS1_SHA256])
481            .ok_or_else(||{
482                errors::non_retryable_from_str("unable to choose RSA_PKCS1_SHA256 signing scheme as it is not supported by current signer")
483            })
484    }
485}
486
487impl std::fmt::Debug for ServiceAccountKey {
488    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
489        f.debug_struct("ServiceAccountKey")
490            .field("client_email", &self.client_email)
491            .field("private_key_id", &self.private_key_id)
492            .field("private_key", &"[censored]")
493            .field("project_id", &self.project_id)
494            .field("universe_domain", &self.universe_domain)
495            .finish()
496    }
497}
498
499#[derive(Debug)]
500struct ServiceAccountCredentials<T>
501where
502    T: CachedTokenProvider,
503{
504    token_provider: T,
505    quota_project_id: Option<String>,
506    universe_domain: Option<String>,
507}
508
509#[derive(Debug)]
510struct ServiceAccountTokenProvider {
511    service_account_key: ServiceAccountKey,
512    access_specifier: AccessSpecifier,
513}
514
515fn token_issue_time(current_time: OffsetDateTime) -> OffsetDateTime {
516    current_time - CLOCK_SKEW_FUDGE
517}
518
519fn token_expiry_time(current_time: OffsetDateTime) -> OffsetDateTime {
520    current_time + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT
521}
522
523#[async_trait]
524impl TokenProvider for ServiceAccountTokenProvider {
525    async fn token(&self) -> Result<Token> {
526        let expires_at = Instant::now() + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT;
527        let tg = ServiceAccountTokenGenerator {
528            audience: self.access_specifier.audience().cloned(),
529            scopes: self
530                .access_specifier
531                .scopes()
532                .map(|scopes| scopes.join(" ")),
533            service_account_key: self.service_account_key.clone(),
534            target_audience: None,
535        };
536
537        let token = tg.generate()?;
538
539        let token = Token {
540            token,
541            token_type: "Bearer".to_string(),
542            expires_at: Some(expires_at),
543            metadata: None,
544        };
545        Ok(token)
546    }
547}
548
549#[derive(Default, Clone)]
550pub(crate) struct ServiceAccountTokenGenerator {
551    service_account_key: ServiceAccountKey,
552    audience: Option<String>,
553    scopes: Option<String>,
554    target_audience: Option<String>,
555}
556
557impl ServiceAccountTokenGenerator {
558    #[cfg(feature = "idtoken")]
559    pub(crate) fn new_id_token_generator(
560        target_audience: String,
561        audience: String,
562        service_account_key: ServiceAccountKey,
563    ) -> Self {
564        Self {
565            service_account_key,
566            target_audience: Some(target_audience),
567            audience: Some(audience),
568            scopes: None,
569        }
570    }
571
572    pub(crate) fn generate(&self) -> Result<String> {
573        let signer = self.service_account_key.signer()?;
574
575        // The claims encode a unix timestamp. `std::time::Instant` has no
576        // epoch, so we use `time::OffsetDateTime`, which reads system time, in
577        // the implementation.
578        let current_time = OffsetDateTime::now_utc();
579
580        let claims = JwsClaims {
581            iss: self.service_account_key.client_email.clone(),
582            scope: self.scopes.clone(),
583            target_audience: self.target_audience.clone(),
584            aud: self.audience.clone(),
585            exp: token_expiry_time(current_time),
586            iat: token_issue_time(current_time),
587            typ: None,
588            sub: Some(self.service_account_key.client_email.clone()),
589        };
590
591        let header = JwsHeader {
592            alg: "RS256",
593            typ: "JWT",
594            kid: Some(self.service_account_key.private_key_id.clone()),
595        };
596        let encoded_header_claims = format!("{}.{}", header.encode()?, claims.encode()?);
597        let sig = signer
598            .sign(encoded_header_claims.as_bytes())
599            .map_err(errors::non_retryable)?;
600        use base64::prelude::{BASE64_URL_SAFE_NO_PAD, Engine as _};
601        let token = format!(
602            "{}.{}",
603            encoded_header_claims,
604            &BASE64_URL_SAFE_NO_PAD.encode(sig)
605        );
606
607        Ok(token)
608    }
609}
610
611#[async_trait::async_trait]
612impl<T> CredentialsProvider for ServiceAccountCredentials<T>
613where
614    T: CachedTokenProvider,
615{
616    async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
617        let token = self.token_provider.token(extensions).await?;
618
619        AuthHeadersBuilder::new(&token)
620            .maybe_quota_project_id(self.quota_project_id.as_deref())
621            .build()
622    }
623
624    async fn universe_domain(&self) -> Option<String> {
625        self.universe_domain.clone()
626    }
627}
628
629#[async_trait::async_trait]
630impl<T> AccessTokenCredentialsProvider for ServiceAccountCredentials<T>
631where
632    T: CachedTokenProvider,
633{
634    async fn access_token(&self) -> Result<AccessToken> {
635        let token = self.token_provider.token(Extensions::new()).await?;
636        token.into()
637    }
638}
639
640#[cfg(test)]
641mod tests {
642    use super::*;
643    use crate::credentials::QUOTA_PROJECT_KEY;
644    use crate::credentials::tests::{
645        PKCS8_PK, b64_decode_to_json, get_headers_from_cache, get_token_from_headers,
646    };
647    use crate::token::tests::MockTokenProvider;
648    use http::HeaderValue;
649    use http::header::AUTHORIZATION;
650    use rsa::pkcs1::EncodeRsaPrivateKey;
651    use rsa::pkcs8::LineEnding;
652    use serde_json::Value;
653    use serde_json::json;
654    use serial_test::parallel;
655    use std::error::Error as _;
656    use std::time::Duration;
657
658    type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
659
660    const SSJ_REGEX: &str = r"(?<header>[^\.]+)\.(?<claims>[^\.]+)\.(?<sig>[^\.]+)";
661
662    #[test]
663    #[parallel]
664    fn debug_token_provider() {
665        let expected = ServiceAccountKey {
666            client_email: "test-client-email".to_string(),
667            private_key_id: "test-private-key-id".to_string(),
668            private_key: "super-duper-secret-private-key".to_string(),
669            project_id: "test-project-id".to_string(),
670            universe_domain: Some("test-universe-domain".to_string()),
671        };
672        let fmt = format!("{expected:?}");
673        assert!(fmt.contains("test-client-email"), "{fmt}");
674        assert!(fmt.contains("test-private-key-id"), "{fmt}");
675        assert!(!fmt.contains("super-duper-secret-private-key"), "{fmt}");
676        assert!(fmt.contains("test-project-id"), "{fmt}");
677        assert!(fmt.contains("test-universe-domain"), "{fmt}");
678    }
679
680    #[test]
681    #[parallel]
682    fn validate_token_issue_time() {
683        let current_time = OffsetDateTime::now_utc();
684        let token_issue_time = token_issue_time(current_time);
685        assert!(token_issue_time == current_time - CLOCK_SKEW_FUDGE);
686    }
687
688    #[test]
689    #[parallel]
690    fn validate_token_expiry_time() {
691        let current_time = OffsetDateTime::now_utc();
692        let token_issue_time = token_expiry_time(current_time);
693        assert!(token_issue_time == current_time + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT);
694    }
695
696    #[tokio::test]
697    #[parallel]
698    async fn headers_success_without_quota_project() -> TestResult {
699        let token = Token {
700            token: "test-token".to_string(),
701            token_type: "Bearer".to_string(),
702            expires_at: None,
703            metadata: None,
704        };
705
706        let mut mock = MockTokenProvider::new();
707        mock.expect_token().times(1).return_once(|| Ok(token));
708
709        let sac = ServiceAccountCredentials {
710            token_provider: TokenCache::new(mock),
711            quota_project_id: None,
712            universe_domain: None,
713        };
714
715        let mut extensions = Extensions::new();
716        let cached_headers = sac.headers(extensions.clone()).await.unwrap();
717        let (headers, entity_tag) = match cached_headers {
718            CacheableResource::New { entity_tag, data } => (data, entity_tag),
719            CacheableResource::NotModified => unreachable!("expecting new headers"),
720        };
721        let token = headers.get(AUTHORIZATION).unwrap();
722
723        assert_eq!(headers.len(), 1, "{headers:?}");
724        assert_eq!(token, HeaderValue::from_static("Bearer test-token"));
725        assert!(token.is_sensitive());
726
727        extensions.insert(entity_tag);
728
729        let cached_headers = sac.headers(extensions).await?;
730
731        match cached_headers {
732            CacheableResource::New { .. } => unreachable!("expecting new headers"),
733            CacheableResource::NotModified => CacheableResource::<HeaderMap>::NotModified,
734        };
735        Ok(())
736    }
737
738    #[tokio::test]
739    #[parallel]
740    async fn headers_success_with_quota_project() -> TestResult {
741        let token = Token {
742            token: "test-token".to_string(),
743            token_type: "Bearer".to_string(),
744            expires_at: None,
745            metadata: None,
746        };
747
748        let quota_project = "test-quota-project";
749
750        let mut mock = MockTokenProvider::new();
751        mock.expect_token().times(1).return_once(|| Ok(token));
752
753        let sac = ServiceAccountCredentials {
754            token_provider: TokenCache::new(mock),
755            quota_project_id: Some(quota_project.to_string()),
756            universe_domain: None,
757        };
758
759        let headers = get_headers_from_cache(sac.headers(Extensions::new()).await.unwrap())?;
760        let token = headers.get(AUTHORIZATION).unwrap();
761        let quota_project_header = headers.get(QUOTA_PROJECT_KEY).unwrap();
762
763        assert_eq!(headers.len(), 2, "{headers:?}");
764        assert_eq!(token, HeaderValue::from_static("Bearer test-token"));
765        assert!(token.is_sensitive());
766        assert_eq!(
767            quota_project_header,
768            HeaderValue::from_static(quota_project)
769        );
770        assert!(!quota_project_header.is_sensitive());
771        Ok(())
772    }
773
774    #[tokio::test]
775    #[parallel]
776    async fn headers_failure() {
777        let mut mock = MockTokenProvider::new();
778        mock.expect_token()
779            .times(1)
780            .return_once(|| Err(errors::non_retryable_from_str("fail")));
781
782        let sac = ServiceAccountCredentials {
783            token_provider: TokenCache::new(mock),
784            quota_project_id: None,
785            universe_domain: None,
786        };
787        let result = sac.headers(Extensions::new()).await;
788        assert!(result.is_err(), "{result:?}");
789    }
790
791    fn get_mock_service_key() -> Value {
792        json!({
793            "client_email": "test-client-email",
794            "private_key_id": "test-private-key-id",
795            "private_key": "",
796            "project_id": "test-project-id",
797        })
798    }
799
800    #[tokio::test]
801    #[parallel]
802    async fn get_service_account_headers_pkcs1_private_key_failure() -> TestResult {
803        let mut service_account_key = get_mock_service_key();
804
805        let key = crate::credentials::tests::RSA_PRIVATE_KEY
806            .to_pkcs1_pem(LineEnding::LF)
807            .expect("Failed to encode key to PKCS#1 PEM")
808            .to_string();
809
810        service_account_key["private_key"] = Value::from(key);
811        let cred = Builder::new(service_account_key).build()?;
812        let expected_error_message = "expected key to be in form of PKCS8, found ";
813        assert!(
814            cred.headers(Extensions::new())
815                .await
816                .is_err_and(|e| e.to_string().contains(expected_error_message))
817        );
818        Ok(())
819    }
820
821    #[tokio::test]
822    #[parallel]
823    async fn get_service_account_token_pkcs8_key_success() -> TestResult {
824        let mut service_account_key = get_mock_service_key();
825        service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
826        let tp = Builder::new(service_account_key.clone()).build_token_provider()?;
827
828        let token = tp.token().await?;
829        let re = regex::Regex::new(SSJ_REGEX).unwrap();
830        let captures = re.captures(&token.token).ok_or_else(|| {
831            format!(
832                r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {}"#,
833                token.token
834            )
835        })?;
836        let header = b64_decode_to_json(captures["header"].to_string());
837        assert_eq!(header["alg"], "RS256");
838        assert_eq!(header["typ"], "JWT");
839        assert_eq!(header["kid"], service_account_key["private_key_id"]);
840
841        let claims = b64_decode_to_json(captures["claims"].to_string());
842        assert_eq!(claims["iss"], service_account_key["client_email"]);
843        assert_eq!(claims["scope"], DEFAULT_SCOPE);
844        assert!(claims["iat"].is_number());
845        assert!(claims["exp"].is_number());
846        assert_eq!(claims["sub"], service_account_key["client_email"]);
847
848        Ok(())
849    }
850
851    #[tokio::test(start_paused = true)]
852    #[parallel]
853    async fn header_caching() -> TestResult {
854        let private_key = PKCS8_PK.clone();
855
856        let json_value = json!({
857            "client_email": "test-client-email",
858            "private_key_id": "test-private-key-id",
859            "private_key": private_key,
860            "project_id": "test-project-id",
861            "universe_domain": "googleapis.com"
862        });
863
864        let credentials = Builder::new(json_value).build()?;
865
866        let headers = credentials.headers(Extensions::new()).await?;
867
868        let re = regex::Regex::new(SSJ_REGEX).unwrap();
869        let token = get_token_from_headers(headers).unwrap();
870
871        let captures = re.captures(&token).unwrap();
872
873        let claims = b64_decode_to_json(captures["claims"].to_string());
874        let first_iat = claims["iat"].as_i64().unwrap();
875
876        // The issued at claim (`iat`) encodes a unix timestamp, in seconds.
877        // Advancing the clock by one second ensures that a subsequent claim has a
878        // different `iat`. Using `tokio::time::advance` changes `Instant::now()` without slowing down the test.
879        tokio::time::advance(Duration::from_secs(1)).await;
880
881        // Get the token again.
882        let token = get_token_from_headers(credentials.headers(Extensions::new()).await?).unwrap();
883        let captures = re.captures(&token).unwrap();
884
885        let claims = b64_decode_to_json(captures["claims"].to_string());
886        let second_iat = claims["iat"].as_i64().unwrap();
887
888        // Validate that the issued at claim is the same for the two tokens. If
889        // the 2nd token is not from the cache, its `iat` will be different.
890        assert_eq!(first_iat, second_iat);
891
892        Ok(())
893    }
894
895    #[tokio::test]
896    #[parallel]
897    async fn universe_domain() -> TestResult {
898        let private_key = PKCS8_PK.clone();
899        // SA key without universe_domain
900        let json_value = json!({
901            "client_email": "test-client-email",
902            "private_key_id": "test-private-key-id",
903            "private_key": private_key,
904            "project_id": "test-project-id",
905        });
906
907        let credentials = Builder::new(json_value).build()?;
908
909        let universe_domain = credentials.universe_domain().await;
910        assert_eq!(universe_domain, None);
911
912        // SA key with universe_domain
913        let json_value = json!({
914            "client_email": "test-client-email",
915            "private_key_id": "test-private-key-id",
916            "private_key": private_key,
917            "project_id": "test-project-id",
918            "universe_domain": "some-universe-domain.com"
919        });
920
921        let credentials = Builder::new(json_value.clone()).build()?;
922
923        let universe_domain = credentials.universe_domain().await;
924        assert_eq!(universe_domain.as_deref(), Some("some-universe-domain.com"));
925
926        let credentials = Builder::new(json_value)
927            .with_universe_domain("other-universe-domain.com")
928            .build()?;
929
930        let universe_domain = credentials.universe_domain().await;
931        assert_eq!(
932            universe_domain.as_deref(),
933            Some("other-universe-domain.com")
934        );
935
936        Ok(())
937    }
938
939    #[tokio::test]
940    #[parallel]
941    async fn get_service_account_headers_invalid_key_failure() -> TestResult {
942        let mut service_account_key = get_mock_service_key();
943        let pem_data = "-----BEGIN PRIVATE KEY-----\nMIGkAg==\n-----END PRIVATE KEY-----";
944        service_account_key["private_key"] = Value::from(pem_data);
945        let cred = Builder::new(service_account_key).build()?;
946
947        let token = cred.headers(Extensions::new()).await;
948        let err = token.unwrap_err();
949        assert!(!err.is_transient(), "{err:?}");
950        let source = err.source().and_then(|e| e.downcast_ref::<rustls::Error>());
951        assert!(matches!(source, Some(rustls::Error::General(_))), "{err:?}");
952        Ok(())
953    }
954
955    #[tokio::test]
956    #[parallel]
957    async fn get_service_account_invalid_json_failure() -> TestResult {
958        let service_account_key = Value::from(" ");
959        let e = Builder::new(service_account_key).build().unwrap_err();
960        assert!(e.is_parsing(), "{e:?}");
961
962        Ok(())
963    }
964
965    #[test]
966    fn signer_failure() -> TestResult {
967        let tp = Builder::new(get_mock_service_key()).build_token_provider()?;
968        let tg = ServiceAccountTokenGenerator {
969            service_account_key: tp.service_account_key.clone(),
970            ..Default::default()
971        };
972
973        let signer = tg.service_account_key.signer();
974        let expected_error_message = "failed to parse service account private key PEM";
975        assert!(signer.is_err_and(|e| e.to_string().contains(expected_error_message)));
976        Ok(())
977    }
978
979    #[test]
980    fn signer_fails_on_invalid_pem_type() -> TestResult {
981        let invalid_pem = concat!(
982            "-----BEGI X509 CRL-----\n",
983            "MIIBmzCBja... (truncated) ...\n",
984            "-----END X509 CRL-----"
985        );
986
987        let mut key = ServiceAccountKey {
988            private_key: invalid_pem.to_string(),
989            ..Default::default()
990        };
991        key.private_key = invalid_pem.to_string();
992        let result = key.signer();
993        assert!(result.is_err(), "{result:?}");
994        let error_msg = result.unwrap_err().to_string();
995        assert!(error_msg.contains("failed to parse service account private key PEM"));
996        Ok(())
997    }
998
999    #[tokio::test]
1000    #[parallel]
1001    async fn get_service_account_headers_with_audience() -> TestResult {
1002        let mut service_account_key = get_mock_service_key();
1003        service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1004        let headers = Builder::new(service_account_key.clone())
1005            .with_access_specifier(AccessSpecifier::from_audience("test-audience"))
1006            .build()?
1007            .headers(Extensions::new())
1008            .await?;
1009
1010        let re = regex::Regex::new(SSJ_REGEX).unwrap();
1011        let token = get_token_from_headers(headers).unwrap();
1012        let captures = re.captures(&token).ok_or_else(|| {
1013            format!(r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {token}"#)
1014        })?;
1015        let token_header = b64_decode_to_json(captures["header"].to_string());
1016        assert_eq!(token_header["alg"], "RS256");
1017        assert_eq!(token_header["typ"], "JWT");
1018        assert_eq!(token_header["kid"], service_account_key["private_key_id"]);
1019
1020        let claims = b64_decode_to_json(captures["claims"].to_string());
1021        assert_eq!(claims["iss"], service_account_key["client_email"]);
1022        assert_eq!(claims["scope"], Value::Null);
1023        assert_eq!(claims["aud"], "test-audience");
1024        assert!(claims["iat"].is_number());
1025        assert!(claims["exp"].is_number());
1026        assert_eq!(claims["sub"], service_account_key["client_email"]);
1027        Ok(())
1028    }
1029
1030    #[tokio::test(start_paused = true)]
1031    #[parallel]
1032    async fn get_service_account_token_verify_expiry_time() -> TestResult {
1033        let now = Instant::now();
1034        let mut service_account_key = get_mock_service_key();
1035        service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1036        let token = Builder::new(service_account_key)
1037            .build_token_provider()?
1038            .token()
1039            .await?;
1040
1041        let expected_expiry = now + CLOCK_SKEW_FUDGE + DEFAULT_TOKEN_TIMEOUT;
1042
1043        assert_eq!(token.expires_at.unwrap(), expected_expiry);
1044        Ok(())
1045    }
1046
1047    #[tokio::test]
1048    #[parallel]
1049    async fn get_service_account_headers_with_custom_scopes() -> TestResult {
1050        let mut service_account_key = get_mock_service_key();
1051        let scopes = vec![
1052            "https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate",
1053        ];
1054        service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1055        let headers = Builder::new(service_account_key.clone())
1056            .with_access_specifier(AccessSpecifier::from_scopes(scopes.clone()))
1057            .build()?
1058            .headers(Extensions::new())
1059            .await?;
1060
1061        let re = regex::Regex::new(SSJ_REGEX).unwrap();
1062        let token = get_token_from_headers(headers).unwrap();
1063        let captures = re.captures(&token).ok_or_else(|| {
1064            format!(r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {token}"#)
1065        })?;
1066        let token_header = b64_decode_to_json(captures["header"].to_string());
1067        assert_eq!(token_header["alg"], "RS256");
1068        assert_eq!(token_header["typ"], "JWT");
1069        assert_eq!(token_header["kid"], service_account_key["private_key_id"]);
1070
1071        let claims = b64_decode_to_json(captures["claims"].to_string());
1072        assert_eq!(claims["iss"], service_account_key["client_email"]);
1073        assert_eq!(claims["scope"], scopes.join(" "));
1074        assert_eq!(claims["aud"], Value::Null);
1075        assert!(claims["iat"].is_number());
1076        assert!(claims["exp"].is_number());
1077        assert_eq!(claims["sub"], service_account_key["client_email"]);
1078        Ok(())
1079    }
1080
1081    #[tokio::test]
1082    #[parallel]
1083    async fn get_service_account_access_token() -> TestResult {
1084        let mut service_account_key = get_mock_service_key();
1085        service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1086        let creds = Builder::new(service_account_key.clone()).build_access_token_credentials()?;
1087
1088        let access_token = creds.access_token().await?;
1089        let token = access_token.token;
1090
1091        let re = regex::Regex::new(SSJ_REGEX).unwrap();
1092        let captures = re.captures(&token).ok_or_else(|| {
1093            format!(r#"Expected token in form: "<header>.<claims>.<sig>". Found token: {token}"#)
1094        })?;
1095        let token_header = b64_decode_to_json(captures["header"].to_string());
1096        assert_eq!(token_header["alg"], "RS256");
1097        assert_eq!(token_header["typ"], "JWT");
1098        assert_eq!(token_header["kid"], service_account_key["private_key_id"]);
1099
1100        Ok(())
1101    }
1102
1103    #[tokio::test]
1104    #[parallel]
1105    async fn get_service_account_signer() -> TestResult {
1106        let mut service_account_key = get_mock_service_key();
1107        service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1108        let signer = Builder::new(service_account_key.clone()).build_signer()?;
1109
1110        let client_email = signer.client_email().await?;
1111        assert_eq!(client_email, service_account_key["client_email"]);
1112
1113        let _bytes = signer.sign(b"test").await?;
1114
1115        Ok(())
1116    }
1117
1118    #[tokio::test]
1119    #[parallel]
1120    #[cfg(google_cloud_unstable_trusted_boundaries)]
1121    async fn e2e_access_boundary() -> TestResult {
1122        use crate::credentials::tests::get_access_boundary_from_headers;
1123        use httptest::responders::json_encoded;
1124        use httptest::{Expectation, Server, matchers::*};
1125        use serde_json::Value;
1126
1127        let mut service_account_key = get_mock_service_key();
1128        service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1129        let email = service_account_key["client_email"].as_str().unwrap();
1130
1131        let server = Server::run();
1132        server.expect(
1133            Expectation::matching(all_of![request::method_path(
1134                "GET",
1135                format!("/v1/projects/-/serviceAccounts/{email}/allowedLocations")
1136            ),])
1137            .times(1)
1138            .respond_with(json_encoded(json!({
1139                "locations": ["us-central1", "us-east1"],
1140                "encodedLocations": "0x1234"
1141            }))),
1142        );
1143
1144        let iam_endpoint = server.url("").to_string().trim_end_matches('/').to_string();
1145
1146        let creds = Builder::new(service_account_key.clone())
1147            .maybe_iam_endpoint_override(Some(iam_endpoint))
1148            .build_credentials()?;
1149
1150        // let the access boundary background thread update
1151        creds.wait_for_boundary().await;
1152
1153        let headers = creds.headers(Extensions::new()).await?;
1154        let token = get_token_from_headers(headers.clone());
1155        let access_boundary = get_access_boundary_from_headers(headers);
1156        assert!(token.is_some(), "should have some token: {token:?}");
1157        assert_eq!(
1158            access_boundary.as_deref(),
1159            Some("0x1234"),
1160            "should be 0x1234 but found: {access_boundary:?}"
1161        );
1162
1163        Ok(())
1164    }
1165}