Skip to main content

google_cloud_auth/credentials/
service_account.rs

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