Skip to main content

google_cloud_auth/
credentials.rs

1// Copyright 2024 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//! Types and functions to work with Google Cloud authentication [Credentials].
16//!
17//! [Credentials]: https://cloud.google.com/docs/authentication#credentials
18
19use crate::build_errors::Error as BuilderError;
20use crate::constants::GOOGLE_CLOUD_QUOTA_PROJECT_VAR;
21use crate::errors::{self, CredentialsError};
22use crate::token::Token;
23use crate::{BuildResult, Result};
24use http::{Extensions, HeaderMap};
25use serde_json::Value;
26use std::future::Future;
27use std::sync::Arc;
28use std::sync::atomic::{AtomicU64, Ordering};
29pub mod anonymous;
30pub mod api_key_credentials;
31pub mod external_account;
32pub(crate) mod external_account_sources;
33#[cfg(feature = "idtoken")]
34pub mod idtoken;
35pub mod impersonated;
36pub(crate) mod internal;
37pub mod mds;
38pub mod service_account;
39pub mod subject_token;
40pub mod user_account;
41pub(crate) const QUOTA_PROJECT_KEY: &str = "x-goog-user-project";
42
43/// Represents an Entity Tag for a [CacheableResource].
44///
45/// An `EntityTag` is an opaque token that can be used to determine if a
46/// cached resource has changed. The specific format of this tag is an
47/// implementation detail.
48///
49/// As the name indicates, these are inspired by the ETags prevalent in HTTP
50/// caching protocols. Their implementation is very different, and are only
51/// intended for use within a single program.
52#[derive(Clone, Debug, PartialEq, Default)]
53pub struct EntityTag(u64);
54
55static ENTITY_TAG_GENERATOR: AtomicU64 = AtomicU64::new(0);
56impl EntityTag {
57    pub fn new() -> Self {
58        let value = ENTITY_TAG_GENERATOR.fetch_add(1, Ordering::SeqCst);
59        Self(value)
60    }
61}
62
63/// Represents a resource that can be cached, along with its [EntityTag].
64///
65/// This enum is used to provide cacheable data to the consumers of the credential provider.
66/// It allows a data provider to return either new data (with an [EntityTag]) or
67/// indicate that the caller's cached version (identified by a previously provided [EntityTag])
68/// is still valid.
69#[derive(Clone, PartialEq, Debug)]
70pub enum CacheableResource<T> {
71    NotModified,
72    New { entity_tag: EntityTag, data: T },
73}
74
75/// An implementation of [crate::credentials::CredentialsProvider].
76///
77/// Represents a [Credentials] used to obtain the auth request headers.
78///
79/// In general, [Credentials][credentials-link] are "digital object that provide
80/// proof of identity", the archetype may be a username and password
81/// combination, but a private RSA key may be a better example.
82///
83/// Modern authentication protocols do not send the credentials to authenticate
84/// with a service. Even when sent over encrypted transports, the credentials
85/// may be accidentally exposed via logging or may be captured if there are
86/// errors in the transport encryption. Because the credentials are often
87/// long-lived, that risk of exposure is also long-lived.
88///
89/// Instead, modern authentication protocols exchange the credentials for a
90/// time-limited [Token][token-link], a digital object that shows the caller was
91/// in possession of the credentials. Because tokens are time limited, risk of
92/// misuse is also time limited. Tokens may be further restricted to only a
93/// certain subset of the RPCs in the service, or even to specific resources, or
94/// only when used from a given machine (virtual or not). Further limiting the
95/// risks associated with any leaks of these tokens.
96///
97/// This struct also abstracts token sources that are not backed by a specific
98/// digital object. The canonical example is the [Metadata Service]. This
99/// service is available in many Google Cloud environments, including
100/// [Google Compute Engine], and [Google Kubernetes Engine].
101///
102/// [credentials-link]: https://cloud.google.com/docs/authentication#credentials
103/// [token-link]: https://cloud.google.com/docs/authentication#token
104/// [Metadata Service]: https://cloud.google.com/compute/docs/metadata/overview
105/// [Google Compute Engine]: https://cloud.google.com/products/compute
106/// [Google Kubernetes Engine]: https://cloud.google.com/kubernetes-engine
107#[derive(Clone, Debug)]
108pub struct Credentials {
109    // We use an `Arc` to hold the inner implementation.
110    //
111    // Credentials may be shared across threads (`Send + Sync`), so an `Rc`
112    // will not do.
113    //
114    // They also need to derive `Clone`, as the
115    // `google_cloud_gax::http_client::ReqwestClient`s which hold them derive `Clone`. So a
116    // `Box` will not do.
117    inner: Arc<dyn dynamic::CredentialsProvider>,
118}
119
120impl<T> std::convert::From<T> for Credentials
121where
122    T: crate::credentials::CredentialsProvider + Send + Sync + 'static,
123{
124    fn from(value: T) -> Self {
125        Self {
126            inner: Arc::new(value),
127        }
128    }
129}
130
131impl Credentials {
132    pub async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
133        self.inner.headers(extensions).await
134    }
135
136    pub async fn universe_domain(&self) -> Option<String> {
137        self.inner.universe_domain().await
138    }
139}
140
141/// An implementation of [crate::credentials::CredentialsProvider] that can also
142/// provide direct access to the underlying access token.
143///
144/// This struct is returned by the `build_access_token_credentials()` method on
145/// the various credential builders. It can be used to obtain an access token
146/// directly via the `access_token()` method, or it can be converted into a `Credentials`
147/// object to be used with the Google Cloud client libraries.
148#[derive(Clone, Debug)]
149pub struct AccessTokenCredentials {
150    // We use an `Arc` to hold the inner implementation.
151    //
152    // AccessTokenCredentials may be shared across threads (`Send + Sync`), so an `Rc`
153    // will not do.
154    //
155    // They also need to derive `Clone`, as the
156    // `google_cloud_gax::http_client::ReqwestClient`s which hold them derive `Clone`. So a
157    // `Box` will not do.
158    inner: Arc<dyn dynamic::AccessTokenCredentialsProvider>,
159}
160
161impl<T> std::convert::From<T> for AccessTokenCredentials
162where
163    T: crate::credentials::AccessTokenCredentialsProvider + Send + Sync + 'static,
164{
165    fn from(value: T) -> Self {
166        Self {
167            inner: Arc::new(value),
168        }
169    }
170}
171
172impl AccessTokenCredentials {
173    pub async fn access_token(&self) -> Result<AccessToken> {
174        self.inner.access_token().await
175    }
176}
177
178/// Makes [AccessTokenCredentials] compatible with clients that expect
179/// a [Credentials] and/or a [CredentialsProvider].
180impl CredentialsProvider for AccessTokenCredentials {
181    async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
182        self.inner.headers(extensions).await
183    }
184
185    async fn universe_domain(&self) -> Option<String> {
186        self.inner.universe_domain().await
187    }
188}
189
190/// Represents an OAuth 2.0 access token.
191#[derive(Clone)]
192pub struct AccessToken {
193    /// The access token string.
194    pub token: String,
195}
196
197impl std::fmt::Debug for AccessToken {
198    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199        f.debug_struct("AccessToken")
200            .field("token", &"[censored]")
201            .finish()
202    }
203}
204
205impl std::convert::From<CacheableResource<Token>> for Result<AccessToken> {
206    fn from(token: CacheableResource<Token>) -> Self {
207        match token {
208            CacheableResource::New { data, .. } => Ok(data.into()),
209            CacheableResource::NotModified => Err(errors::CredentialsError::from_msg(
210                false,
211                "Expecting token to be present",
212            )),
213        }
214    }
215}
216
217impl std::convert::From<Token> for AccessToken {
218    fn from(token: Token) -> Self {
219        Self { token: token.token }
220    }
221}
222
223/// A trait for credential types that can provide direct access to an access token.
224///
225/// This trait is primarily intended for interoperability with other libraries that
226/// require a raw access token, or for calling Google Cloud APIs that are not yet
227/// supported by the SDK.
228pub trait AccessTokenCredentialsProvider: CredentialsProvider + std::fmt::Debug {
229    /// Asynchronously retrieves an access token.
230    fn access_token(&self) -> impl Future<Output = Result<AccessToken>> + Send;
231}
232
233/// Represents a [Credentials] used to obtain auth request headers.
234///
235/// In general, [Credentials][credentials-link] are "digital object that
236/// provide proof of identity", the archetype may be a username and password
237/// combination, but a private RSA key may be a better example.
238///
239/// Modern authentication protocols do not send the credentials to
240/// authenticate with a service. Even when sent over encrypted transports,
241/// the credentials may be accidentally exposed via logging or may be
242/// captured if there are errors in the transport encryption. Because the
243/// credentials are often long-lived, that risk of exposure is also
244/// long-lived.
245///
246/// Instead, modern authentication protocols exchange the credentials for a
247/// time-limited [Token][token-link], a digital object that shows the caller
248/// was in possession of the credentials. Because tokens are time limited,
249/// risk of misuse is also time limited. Tokens may be further restricted to
250/// only a certain subset of the RPCs in the service, or even to specific
251/// resources, or only when used from a given machine (virtual or not).
252/// Further limiting the risks associated with any leaks of these tokens.
253///
254/// This struct also abstracts token sources that are not backed by a
255/// specific digital object. The canonical example is the
256/// [Metadata Service]. This service is available in many Google Cloud
257/// environments, including [Google Compute Engine], and
258/// [Google Kubernetes Engine].
259///
260/// # Notes
261///
262/// Application developers who directly use the Auth SDK can use this trait,
263/// along with [crate::credentials::Credentials::from()] to mock the credentials.
264/// Application developers who use the Google Cloud Rust SDK directly should not
265/// need this functionality.
266///
267/// [credentials-link]: https://cloud.google.com/docs/authentication#credentials
268/// [token-link]: https://cloud.google.com/docs/authentication#token
269/// [Metadata Service]: https://cloud.google.com/compute/docs/metadata/overview
270/// [Google Compute Engine]: https://cloud.google.com/products/compute
271/// [Google Kubernetes Engine]: https://cloud.google.com/kubernetes-engine
272pub trait CredentialsProvider: std::fmt::Debug {
273    /// Asynchronously constructs the auth headers.
274    ///
275    /// Different auth tokens are sent via different headers. The
276    /// [Credentials] constructs the headers (and header values) that should be
277    /// sent with a request.
278    ///
279    /// # Parameters
280    /// * `extensions` - An `http::Extensions` map that can be used to pass additional
281    ///   context to the credential provider. For caching purposes, this can include
282    ///   an [EntityTag] from a previously returned [`CacheableResource<HeaderMap>`].
283    ///   If a valid `EntityTag` is provided and the underlying authentication data
284    ///   has not changed, this method returns `Ok(CacheableResource::NotModified)`.
285    ///
286    /// # Returns
287    /// A `Future` that resolves to a `Result` containing:
288    /// * `Ok(CacheableResource::New { entity_tag, data })`: If new or updated headers
289    ///   are available.
290    /// * `Ok(CacheableResource::NotModified)`: If the headers have not changed since
291    ///   the ETag provided via `extensions` was issued.
292    /// * `Err(CredentialsError)`: If an error occurs while trying to fetch or
293    ///   generating the headers.
294    fn headers(
295        &self,
296        extensions: Extensions,
297    ) -> impl Future<Output = Result<CacheableResource<HeaderMap>>> + Send;
298
299    /// Retrieves the universe domain associated with the credentials, if any.
300    fn universe_domain(&self) -> impl Future<Output = Option<String>> + Send;
301}
302
303pub(crate) mod dynamic {
304    use super::Result;
305    use super::{CacheableResource, Extensions, HeaderMap};
306
307    /// A dyn-compatible, crate-private version of `CredentialsProvider`.
308    #[async_trait::async_trait]
309    pub trait CredentialsProvider: Send + Sync + std::fmt::Debug {
310        /// Asynchronously constructs the auth headers.
311        ///
312        /// Different auth tokens are sent via different headers. The
313        /// [Credentials] constructs the headers (and header values) that should be
314        /// sent with a request.
315        ///
316        /// # Parameters
317        /// * `extensions` - An `http::Extensions` map that can be used to pass additional
318        ///   context to the credential provider. For caching purposes, this can include
319        ///   an [EntityTag] from a previously returned [CacheableResource<HeaderMap>].
320        ///   If a valid `EntityTag` is provided and the underlying authentication data
321        ///   has not changed, this method returns `Ok(CacheableResource::NotModified)`.
322        ///
323        /// # Returns
324        /// A `Future` that resolves to a `Result` containing:
325        /// * `Ok(CacheableResource::New { entity_tag, data })`: If new or updated headers
326        ///   are available.
327        /// * `Ok(CacheableResource::NotModified)`: If the headers have not changed since
328        ///   the ETag provided via `extensions` was issued.
329        /// * `Err(CredentialsError)`: If an error occurs while trying to fetch or
330        ///   generating the headers.
331        async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>>;
332
333        /// Retrieves the universe domain associated with the credentials, if any.
334        async fn universe_domain(&self) -> Option<String> {
335            Some("googleapis.com".to_string())
336        }
337    }
338
339    /// The public CredentialsProvider implements the dyn-compatible CredentialsProvider.
340    #[async_trait::async_trait]
341    impl<T> CredentialsProvider for T
342    where
343        T: super::CredentialsProvider + Send + Sync,
344    {
345        async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
346            T::headers(self, extensions).await
347        }
348        async fn universe_domain(&self) -> Option<String> {
349            T::universe_domain(self).await
350        }
351    }
352
353    /// A dyn-compatible, crate-private version of `AccessTokenCredentialsProvider`.
354    #[async_trait::async_trait]
355    pub trait AccessTokenCredentialsProvider:
356        CredentialsProvider + Send + Sync + std::fmt::Debug
357    {
358        async fn access_token(&self) -> Result<super::AccessToken>;
359    }
360
361    #[async_trait::async_trait]
362    impl<T> AccessTokenCredentialsProvider for T
363    where
364        T: super::AccessTokenCredentialsProvider + Send + Sync,
365    {
366        async fn access_token(&self) -> Result<super::AccessToken> {
367            T::access_token(self).await
368        }
369    }
370}
371
372/// A builder for constructing [`Credentials`] instances.
373///
374/// This builder loads credentials according to the standard
375/// [Application Default Credentials (ADC)][ADC-link] strategy.
376/// ADC is the recommended approach for most applications and conforms to
377/// [AIP-4110]. If you need to load credentials from a non-standard location
378/// or source, you can use Builders on the specific credential types.
379///
380/// Common use cases where using ADC would is useful include:
381/// - Your application is deployed to a Google Cloud environment such as
382///   [Google Compute Engine (GCE)][gce-link],
383///   [Google Kubernetes Engine (GKE)][gke-link], or [Cloud Run]. Each of these
384///   deployment environments provides a default service account to the
385///   application, and offers mechanisms to change this default service account
386///   without any code changes to your application.
387/// - You are testing or developing the application on a workstation (physical
388///   or virtual). These credentials will use your preferences as set with
389///   [gcloud auth application-default]. These preferences can be your own
390///   Google Cloud user credentials, or some service account.
391/// - Regardless of where your application is running, you can use the
392///   `GOOGLE_APPLICATION_CREDENTIALS` environment variable to override the
393///   defaults. This environment variable should point to a file containing a
394///   service account key file, or a JSON object describing your user
395///   credentials.
396///
397/// The headers returned by these credentials should be used in the
398/// Authorization HTTP header.
399///
400/// The Google Cloud client libraries for Rust will typically find and use these
401/// credentials automatically if a credentials file exists in the standard ADC
402/// search paths. You might instantiate these credentials if you need to:
403/// - Override the OAuth 2.0 **scopes** being requested for the access token.
404/// - Override the **quota project ID** for billing and quota management.
405///
406/// # Example: fetching headers using ADC
407/// ```
408/// # use google_cloud_auth::credentials::Builder;
409/// # use http::Extensions;
410/// # async fn sample() -> anyhow::Result<()> {
411/// let credentials = Builder::default()
412///     .with_quota_project_id("my-project")
413///     .build()?;
414/// let headers = credentials.headers(Extensions::new()).await?;
415/// println!("Headers: {headers:?}");
416/// # Ok(()) }
417/// ```
418///
419/// [ADC-link]: https://cloud.google.com/docs/authentication/application-default-credentials
420/// [AIP-4110]: https://google.aip.dev/auth/4110
421/// [Cloud Run]: https://cloud.google.com/run
422/// [gce-link]: https://cloud.google.com/products/compute
423/// [gcloud auth application-default]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default
424/// [gke-link]: https://cloud.google.com/kubernetes-engine
425#[derive(Debug)]
426pub struct Builder {
427    quota_project_id: Option<String>,
428    scopes: Option<Vec<String>>,
429}
430
431impl Default for Builder {
432    /// Creates a new builder where credentials will be obtained via [application-default login].
433    ///
434    /// # Example
435    /// ```
436    /// # use google_cloud_auth::credentials::Builder;
437    /// # fn sample() -> anyhow::Result<()> {
438    /// let credentials = Builder::default().build()?;
439    /// # Ok(()) }
440    /// ```
441    ///
442    /// [application-default login]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login
443    fn default() -> Self {
444        Self {
445            quota_project_id: None,
446            scopes: None,
447        }
448    }
449}
450
451impl Builder {
452    /// Sets the [quota project] for these credentials.
453    ///
454    /// In some services, you can use an account in one project for authentication
455    /// and authorization, and charge the usage to a different project. This requires
456    /// that the user has `serviceusage.services.use` permissions on the quota project.
457    ///
458    /// ## Important: Precedence
459    /// If the `GOOGLE_CLOUD_QUOTA_PROJECT` environment variable is set,
460    /// its value will be used **instead of** the value provided to this method.
461    ///
462    /// # Example
463    /// ```
464    /// # use google_cloud_auth::credentials::Builder;
465    /// # fn sample() -> anyhow::Result<()> {
466    /// let credentials = Builder::default()
467    ///     .with_quota_project_id("my-project")
468    ///     .build()?;
469    /// # Ok(()) }
470    /// ```
471    ///
472    /// [quota project]: https://cloud.google.com/docs/quotas/quota-project
473    pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
474        self.quota_project_id = Some(quota_project_id.into());
475        self
476    }
477
478    /// Sets the [scopes] for these credentials.
479    ///
480    /// `scopes` act as an additional restriction in addition to the IAM permissions
481    /// granted to the principal (user or service account) that creates the token.
482    ///
483    /// `scopes` define the *permissions being requested* for this specific access token
484    /// when interacting with a service. For example,
485    /// `https://www.googleapis.com/auth/devstorage.read_write`.
486    ///
487    /// IAM permissions, on the other hand, define the *underlying capabilities*
488    /// the principal possesses within a system. For example, `storage.buckets.delete`.
489    ///
490    /// The credentials certify that a particular token was created by a certain principal.
491    ///
492    /// When a token generated with specific scopes is used, the request must be permitted
493    /// by both the the principals's underlying IAM permissions and the scopes requested
494    /// for the token.
495    ///
496    /// [scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
497    pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
498    where
499        I: IntoIterator<Item = S>,
500        S: Into<String>,
501    {
502        self.scopes = Some(scopes.into_iter().map(|s| s.into()).collect());
503        self
504    }
505
506    /// Returns a [Credentials] instance with the configured settings.
507    ///
508    /// # Errors
509    ///
510    /// Returns a [CredentialsError] if an unsupported credential type is provided
511    /// or if the JSON value is either malformed or missing required fields.
512    ///
513    /// For more information, on how to generate the JSON for a credential,
514    /// consult the relevant section in the [application-default credentials] guide.
515    ///
516    /// [application-default credentials]: https://cloud.google.com/docs/authentication/application-default-credentials
517    pub fn build(self) -> BuildResult<Credentials> {
518        Ok(self.build_access_token_credentials()?.into())
519    }
520
521    /// Returns an [AccessTokenCredentials] instance with the configured settings.
522    ///
523    /// # Example
524    ///
525    /// ```
526    /// # use google_cloud_auth::credentials::{Builder, AccessTokenCredentials, AccessTokenCredentialsProvider};
527    /// # async fn sample() -> anyhow::Result<()> {
528    /// // This will search for Application Default Credentials and build AccessTokenCredentials.
529    /// let credentials: AccessTokenCredentials = Builder::default()
530    ///     .build_access_token_credentials()?;
531    /// let access_token = credentials.access_token().await?;
532    /// println!("Token: {}", access_token.token);
533    /// # Ok(()) }
534    /// ```
535    ///
536    /// # Errors
537    ///
538    /// Returns a [CredentialsError] if an unsupported credential type is provided
539    /// or if the JSON value is either malformed or missing required fields.
540    ///
541    /// For more information, on how to generate the JSON for a credential,
542    /// consult the relevant section in the [application-default credentials] guide.
543    ///
544    /// [application-default credentials]: https://cloud.google.com/docs/authentication/application-default-credentials
545    pub fn build_access_token_credentials(self) -> BuildResult<AccessTokenCredentials> {
546        let json_data = match load_adc()? {
547            AdcContents::Contents(contents) => {
548                Some(serde_json::from_str(&contents).map_err(BuilderError::parsing)?)
549            }
550            AdcContents::FallbackToMds => None,
551        };
552        let quota_project_id = std::env::var(GOOGLE_CLOUD_QUOTA_PROJECT_VAR)
553            .ok()
554            .or(self.quota_project_id);
555        build_credentials(json_data, quota_project_id, self.scopes)
556    }
557
558    /// Returns a [crate::signer::Signer] instance with the configured settings.
559    ///
560    /// This method automatically loads Application Default Credentials (ADC)
561    /// from the environment and uses them to create a signer.
562    ///
563    /// The returned [crate::signer::Signer] might perform signing locally (e.g. if a service
564    /// account key is found) or via a remote API (e.g. if running on GCE).
565    ///
566    /// # Example
567    ///
568    /// ```
569    /// # use google_cloud_auth::credentials::Builder;
570    /// # use google_cloud_auth::signer::Signer;
571    /// # fn sample() -> anyhow::Result<()> {
572    /// let signer: Signer = Builder::default().build_signer()?;
573    /// # Ok(()) }
574    /// ```
575    pub fn build_signer(self) -> BuildResult<crate::signer::Signer> {
576        let json_data = match load_adc()? {
577            AdcContents::Contents(contents) => {
578                Some(serde_json::from_str(&contents).map_err(BuilderError::parsing)?)
579            }
580            AdcContents::FallbackToMds => None,
581        };
582        let quota_project_id = std::env::var(GOOGLE_CLOUD_QUOTA_PROJECT_VAR)
583            .ok()
584            .or(self.quota_project_id);
585        build_signer(json_data, quota_project_id, self.scopes)
586    }
587}
588
589#[derive(Debug, PartialEq)]
590enum AdcPath {
591    FromEnv(String),
592    WellKnown(String),
593}
594
595#[derive(Debug, PartialEq)]
596enum AdcContents {
597    Contents(String),
598    FallbackToMds,
599}
600
601fn extract_credential_type(json: &Value) -> BuildResult<&str> {
602    json.get("type")
603        .ok_or_else(|| BuilderError::parsing("no `type` field found."))?
604        .as_str()
605        .ok_or_else(|| BuilderError::parsing("`type` field is not a string."))
606}
607
608/// Applies common optional configurations (quota project ID, scopes) to a
609/// specific credential builder instance and then builds it.
610///
611/// This macro centralizes the logic for optionally calling `.with_quota_project_id()`
612/// and `.with_scopes()` on different underlying credential builders (like
613/// `mds::Builder`, `service_account::Builder`, etc.) before calling `.build()`.
614/// It helps avoid repetitive code in the `build_credentials` function.
615macro_rules! config_builder {
616    ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $apply_scopes_closure:expr) => {{
617        let builder = config_common_builder!(
618            $builder_instance,
619            $quota_project_id_option,
620            $scopes_option,
621            $apply_scopes_closure
622        );
623        builder.build_access_token_credentials()
624    }};
625}
626
627/// Applies common optional configurations (quota project ID, scopes) to a
628/// specific credential builder instance and then return a signer for it.
629macro_rules! config_signer {
630    ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $apply_scopes_closure:expr) => {{
631        let builder = config_common_builder!(
632            $builder_instance,
633            $quota_project_id_option,
634            $scopes_option,
635            $apply_scopes_closure
636        );
637        builder.build_signer()
638    }};
639}
640
641macro_rules! config_common_builder {
642    ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $apply_scopes_closure:expr) => {{
643        let builder = $builder_instance;
644        let builder = $quota_project_id_option
645            .into_iter()
646            .fold(builder, |b, qp| b.with_quota_project_id(qp));
647
648        let builder = $scopes_option
649            .into_iter()
650            .fold(builder, |b, s| $apply_scopes_closure(b, s));
651
652        builder
653    }};
654}
655
656fn build_credentials(
657    json: Option<Value>,
658    quota_project_id: Option<String>,
659    scopes: Option<Vec<String>>,
660) -> BuildResult<AccessTokenCredentials> {
661    match json {
662        None => config_builder!(
663            mds::Builder::from_adc(),
664            quota_project_id,
665            scopes,
666            |b: mds::Builder, s: Vec<String>| b.with_scopes(s)
667        ),
668        Some(json) => {
669            let cred_type = extract_credential_type(&json)?;
670            match cred_type {
671                "authorized_user" => {
672                    config_builder!(
673                        user_account::Builder::new(json),
674                        quota_project_id,
675                        scopes,
676                        |b: user_account::Builder, s: Vec<String>| b.with_scopes(s)
677                    )
678                }
679                "service_account" => config_builder!(
680                    service_account::Builder::new(json),
681                    quota_project_id,
682                    scopes,
683                    |b: service_account::Builder, s: Vec<String>| b
684                        .with_access_specifier(service_account::AccessSpecifier::from_scopes(s))
685                ),
686                "impersonated_service_account" => {
687                    config_builder!(
688                        impersonated::Builder::new(json),
689                        quota_project_id,
690                        scopes,
691                        |b: impersonated::Builder, s: Vec<String>| b.with_scopes(s)
692                    )
693                }
694                "external_account" => config_builder!(
695                    external_account::Builder::new(json),
696                    quota_project_id,
697                    scopes,
698                    |b: external_account::Builder, s: Vec<String>| b.with_scopes(s)
699                ),
700                _ => Err(BuilderError::unknown_type(cred_type)),
701            }
702        }
703    }
704}
705
706fn build_signer(
707    json: Option<Value>,
708    quota_project_id: Option<String>,
709    scopes: Option<Vec<String>>,
710) -> BuildResult<crate::signer::Signer> {
711    match json {
712        None => config_signer!(
713            mds::Builder::from_adc(),
714            quota_project_id,
715            scopes,
716            |b: mds::Builder, s: Vec<String>| b.with_scopes(s)
717        ),
718        Some(json) => {
719            let cred_type = extract_credential_type(&json)?;
720            match cred_type {
721                "authorized_user" => Err(BuilderError::not_supported(
722                    "authorized_user signer is not supported",
723                )),
724                "service_account" => config_signer!(
725                    service_account::Builder::new(json),
726                    quota_project_id,
727                    scopes,
728                    |b: service_account::Builder, s: Vec<String>| b
729                        .with_access_specifier(service_account::AccessSpecifier::from_scopes(s))
730                ),
731                "impersonated_service_account" => {
732                    config_signer!(
733                        impersonated::Builder::new(json),
734                        quota_project_id,
735                        scopes,
736                        |b: impersonated::Builder, s: Vec<String>| b.with_scopes(s)
737                    )
738                }
739                "external_account" => Err(BuilderError::not_supported(
740                    "external_account signer is not supported",
741                )),
742                _ => Err(BuilderError::unknown_type(cred_type)),
743            }
744        }
745    }
746}
747
748fn path_not_found(path: String) -> BuilderError {
749    BuilderError::loading(format!(
750        "{path}. {}",
751        concat!(
752            "This file name was found in the `GOOGLE_APPLICATION_CREDENTIALS` ",
753            "environment variable. Verify this environment variable points to ",
754            "a valid file."
755        )
756    ))
757}
758
759fn load_adc() -> BuildResult<AdcContents> {
760    match adc_path() {
761        None => Ok(AdcContents::FallbackToMds),
762        Some(AdcPath::FromEnv(path)) => match std::fs::read_to_string(&path) {
763            Ok(contents) => Ok(AdcContents::Contents(contents)),
764            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(path_not_found(path)),
765            Err(e) => Err(BuilderError::loading(e)),
766        },
767        Some(AdcPath::WellKnown(path)) => match std::fs::read_to_string(path) {
768            Ok(contents) => Ok(AdcContents::Contents(contents)),
769            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(AdcContents::FallbackToMds),
770            Err(e) => Err(BuilderError::loading(e)),
771        },
772    }
773}
774
775/// The path to Application Default Credentials (ADC), as specified in [AIP-4110].
776///
777/// [AIP-4110]: https://google.aip.dev/auth/4110
778fn adc_path() -> Option<AdcPath> {
779    if let Ok(path) = std::env::var("GOOGLE_APPLICATION_CREDENTIALS") {
780        return Some(AdcPath::FromEnv(path));
781    }
782    Some(AdcPath::WellKnown(adc_well_known_path()?))
783}
784
785/// The well-known path to ADC on Windows, as specified in [AIP-4113].
786///
787/// [AIP-4113]: https://google.aip.dev/auth/4113
788#[cfg(target_os = "windows")]
789fn adc_well_known_path() -> Option<String> {
790    std::env::var("APPDATA")
791        .ok()
792        .map(|root| root + "/gcloud/application_default_credentials.json")
793}
794
795/// The well-known path to ADC on Linux and Mac, as specified in [AIP-4113].
796///
797/// [AIP-4113]: https://google.aip.dev/auth/4113
798#[cfg(not(target_os = "windows"))]
799fn adc_well_known_path() -> Option<String> {
800    std::env::var("HOME")
801        .ok()
802        .map(|root| root + "/.config/gcloud/application_default_credentials.json")
803}
804
805/// A module providing invalid credentials where authentication does not matter.
806///
807/// These credentials are a convenient way to avoid errors from loading
808/// Application Default Credentials in tests.
809///
810/// This module is mainly relevant to other `google-cloud-*` crates, but some
811/// external developers (i.e. consumers, not developers of `google-cloud-rust`)
812/// may find it useful.
813// Skipping mutation testing for this module. As it exclusively provides
814// hardcoded credential stubs for testing purposes.
815#[cfg_attr(test, mutants::skip)]
816#[doc(hidden)]
817pub mod testing {
818    use super::CacheableResource;
819    use crate::Result;
820    use crate::credentials::Credentials;
821    use crate::credentials::dynamic::CredentialsProvider;
822    use http::{Extensions, HeaderMap};
823    use std::sync::Arc;
824
825    /// A simple credentials implementation to use in tests.
826    ///
827    /// Always return an error in `headers()`.
828    pub fn error_credentials(retryable: bool) -> Credentials {
829        Credentials {
830            inner: Arc::from(ErrorCredentials(retryable)),
831        }
832    }
833
834    #[derive(Debug, Default)]
835    struct ErrorCredentials(bool);
836
837    #[async_trait::async_trait]
838    impl CredentialsProvider for ErrorCredentials {
839        async fn headers(&self, _extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
840            Err(super::CredentialsError::from_msg(self.0, "test-only"))
841        }
842
843        async fn universe_domain(&self) -> Option<String> {
844            None
845        }
846    }
847}
848
849#[cfg(test)]
850pub(crate) mod tests {
851    use super::*;
852    use crate::constants::TRUST_BOUNDARY_HEADER;
853    use crate::errors::is_gax_error_retryable;
854    use base64::Engine;
855    use google_cloud_gax::backoff_policy::BackoffPolicy;
856    use google_cloud_gax::retry_policy::RetryPolicy;
857    use google_cloud_gax::retry_result::RetryResult;
858    use google_cloud_gax::retry_state::RetryState;
859    use google_cloud_gax::retry_throttler::RetryThrottler;
860    use mockall::mock;
861    use reqwest::header::AUTHORIZATION;
862    use rsa::BigUint;
863    use rsa::RsaPrivateKey;
864    use rsa::pkcs8::{EncodePrivateKey, LineEnding};
865    use scoped_env::ScopedEnv;
866    use std::error::Error;
867    use std::sync::LazyLock;
868    use test_case::test_case;
869    use tokio::time::Duration;
870    use tokio::time::Instant;
871
872    // find the last/root error in the chain that matches the given type
873    pub(crate) fn find_source_error<'a, T: Error + 'static>(
874        error: &'a (dyn Error + 'static),
875    ) -> Option<&'a T> {
876        let mut last_err = None;
877        let mut source = error.source();
878        while let Some(err) = source {
879            if let Some(target_err) = err.downcast_ref::<T>() {
880                last_err = Some(target_err);
881            }
882            source = err.source();
883        }
884        last_err
885    }
886
887    mock! {
888        #[derive(Debug)]
889        pub RetryPolicy {}
890        impl RetryPolicy for RetryPolicy {
891            fn on_error(
892                &self,
893                state: &RetryState,
894                error: google_cloud_gax::error::Error,
895            ) -> RetryResult;
896        }
897    }
898
899    mock! {
900        #[derive(Debug)]
901        pub BackoffPolicy {}
902        impl BackoffPolicy for BackoffPolicy {
903            fn on_failure(&self, state: &RetryState) -> std::time::Duration;
904        }
905    }
906
907    mockall::mock! {
908        #[derive(Debug)]
909        pub RetryThrottler {}
910        impl RetryThrottler for RetryThrottler {
911            fn throttle_retry_attempt(&self) -> bool;
912            fn on_retry_failure(&mut self, error: &RetryResult);
913            fn on_success(&mut self);
914        }
915    }
916
917    type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
918
919    pub(crate) fn get_mock_auth_retry_policy(attempts: usize) -> MockRetryPolicy {
920        let mut retry_policy = MockRetryPolicy::new();
921        retry_policy
922            .expect_on_error()
923            .returning(move |state, error| {
924                if state.attempt_count >= attempts as u32 {
925                    return RetryResult::Exhausted(error);
926                }
927                let is_retryable = is_gax_error_retryable(&error);
928                if is_retryable {
929                    RetryResult::Continue(error)
930                } else {
931                    RetryResult::Permanent(error)
932                }
933            });
934        retry_policy
935    }
936
937    pub(crate) fn get_mock_backoff_policy() -> MockBackoffPolicy {
938        let mut backoff_policy = MockBackoffPolicy::new();
939        backoff_policy
940            .expect_on_failure()
941            .return_const(Duration::from_secs(0));
942        backoff_policy
943    }
944
945    pub(crate) fn get_mock_retry_throttler() -> MockRetryThrottler {
946        let mut throttler = MockRetryThrottler::new();
947        throttler.expect_on_retry_failure().return_const(());
948        throttler
949            .expect_throttle_retry_attempt()
950            .return_const(false);
951        throttler.expect_on_success().return_const(());
952        throttler
953    }
954
955    pub(crate) fn get_headers_from_cache(
956        headers: CacheableResource<HeaderMap>,
957    ) -> Result<HeaderMap> {
958        match headers {
959            CacheableResource::New { data, .. } => Ok(data),
960            CacheableResource::NotModified => Err(CredentialsError::from_msg(
961                false,
962                "Expecting headers to be present",
963            )),
964        }
965    }
966
967    pub(crate) fn get_token_from_headers(headers: CacheableResource<HeaderMap>) -> Option<String> {
968        match headers {
969            CacheableResource::New { data, .. } => data
970                .get(AUTHORIZATION)
971                .and_then(|token_value| token_value.to_str().ok())
972                .and_then(|s| s.split_whitespace().nth(1))
973                .map(|s| s.to_string()),
974            CacheableResource::NotModified => None,
975        }
976    }
977
978    pub(crate) fn get_access_boundary_from_headers(
979        headers: CacheableResource<HeaderMap>,
980    ) -> Option<String> {
981        match headers {
982            CacheableResource::New { data, .. } => data
983                .get(TRUST_BOUNDARY_HEADER)
984                .and_then(|token_value| token_value.to_str().ok())
985                .map(|s| s.to_string()),
986            CacheableResource::NotModified => None,
987        }
988    }
989
990    pub(crate) fn get_token_type_from_headers(
991        headers: CacheableResource<HeaderMap>,
992    ) -> Option<String> {
993        match headers {
994            CacheableResource::New { data, .. } => data
995                .get(AUTHORIZATION)
996                .and_then(|token_value| token_value.to_str().ok())
997                .and_then(|s| s.split_whitespace().next())
998                .map(|s| s.to_string()),
999            CacheableResource::NotModified => None,
1000        }
1001    }
1002
1003    pub static RSA_PRIVATE_KEY: LazyLock<RsaPrivateKey> = LazyLock::new(|| {
1004        let p_str: &str = "141367881524527794394893355677826002829869068195396267579403819572502936761383874443619453704612633353803671595972343528718438130450055151198231345212263093247511629886734453413988207866331439612464122904648042654465604881130663408340669956544709445155137282157402427763452856646879397237752891502149781819597";
1005        let q_str: &str = "179395413952110013801471600075409598322058038890563483332288896635704255883613060744402506322679437982046475766067250097809676406576067239936945362857700460740092421061356861438909617220234758121022105150630083703531219941303688818533566528599328339894969707615478438750812672509434761181735933851075292740309";
1006        let e_str: &str = "65537";
1007
1008        let p = BigUint::parse_bytes(p_str.as_bytes(), 10).expect("Failed to parse prime P");
1009        let q = BigUint::parse_bytes(q_str.as_bytes(), 10).expect("Failed to parse prime Q");
1010        let public_exponent =
1011            BigUint::parse_bytes(e_str.as_bytes(), 10).expect("Failed to parse public exponent");
1012
1013        RsaPrivateKey::from_primes(vec![p, q], public_exponent)
1014            .expect("Failed to create RsaPrivateKey from primes")
1015    });
1016
1017    #[cfg(feature = "idtoken")]
1018    pub static ES256_PRIVATE_KEY: LazyLock<p256::SecretKey> = LazyLock::new(|| {
1019        let secret_key_bytes = [
1020            0x4c, 0x0c, 0x11, 0x6e, 0x6e, 0xb0, 0x07, 0xbd, 0x48, 0x0c, 0xc0, 0x48, 0xc0, 0x1f,
1021            0xac, 0x3d, 0x82, 0x82, 0x0e, 0x6c, 0x3d, 0x76, 0x61, 0x4d, 0x06, 0x4e, 0xdb, 0x05,
1022            0x26, 0x6c, 0x75, 0xdf,
1023        ];
1024        p256::SecretKey::from_bytes((&secret_key_bytes).into()).unwrap()
1025    });
1026
1027    pub static PKCS8_PK: LazyLock<String> = LazyLock::new(|| {
1028        RSA_PRIVATE_KEY
1029            .to_pkcs8_pem(LineEnding::LF)
1030            .expect("Failed to encode key to PKCS#8 PEM")
1031            .to_string()
1032    });
1033
1034    pub fn b64_decode_to_json(s: String) -> serde_json::Value {
1035        let decoded = String::from_utf8(
1036            base64::engine::general_purpose::URL_SAFE_NO_PAD
1037                .decode(s)
1038                .unwrap(),
1039        )
1040        .unwrap();
1041        serde_json::from_str(&decoded).unwrap()
1042    }
1043
1044    #[cfg(target_os = "windows")]
1045    #[test]
1046    #[serial_test::serial]
1047    fn adc_well_known_path_windows() {
1048        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1049        let _appdata = ScopedEnv::set("APPDATA", "C:/Users/foo");
1050        assert_eq!(
1051            adc_well_known_path(),
1052            Some("C:/Users/foo/gcloud/application_default_credentials.json".to_string())
1053        );
1054        assert_eq!(
1055            adc_path(),
1056            Some(AdcPath::WellKnown(
1057                "C:/Users/foo/gcloud/application_default_credentials.json".to_string()
1058            ))
1059        );
1060    }
1061
1062    #[cfg(target_os = "windows")]
1063    #[test]
1064    #[serial_test::serial]
1065    fn adc_well_known_path_windows_no_appdata() {
1066        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1067        let _appdata = ScopedEnv::remove("APPDATA");
1068        assert_eq!(adc_well_known_path(), None);
1069        assert_eq!(adc_path(), None);
1070    }
1071
1072    #[cfg(not(target_os = "windows"))]
1073    #[test]
1074    #[serial_test::serial]
1075    fn adc_well_known_path_posix() {
1076        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1077        let _home = ScopedEnv::set("HOME", "/home/foo");
1078        assert_eq!(
1079            adc_well_known_path(),
1080            Some("/home/foo/.config/gcloud/application_default_credentials.json".to_string())
1081        );
1082        assert_eq!(
1083            adc_path(),
1084            Some(AdcPath::WellKnown(
1085                "/home/foo/.config/gcloud/application_default_credentials.json".to_string()
1086            ))
1087        );
1088    }
1089
1090    #[cfg(not(target_os = "windows"))]
1091    #[test]
1092    #[serial_test::serial]
1093    fn adc_well_known_path_posix_no_home() {
1094        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1095        let _appdata = ScopedEnv::remove("HOME");
1096        assert_eq!(adc_well_known_path(), None);
1097        assert_eq!(adc_path(), None);
1098    }
1099
1100    #[test]
1101    #[serial_test::serial]
1102    fn adc_path_from_env() {
1103        let _creds = ScopedEnv::set(
1104            "GOOGLE_APPLICATION_CREDENTIALS",
1105            "/usr/bar/application_default_credentials.json",
1106        );
1107        assert_eq!(
1108            adc_path(),
1109            Some(AdcPath::FromEnv(
1110                "/usr/bar/application_default_credentials.json".to_string()
1111            ))
1112        );
1113    }
1114
1115    #[test]
1116    #[serial_test::serial]
1117    fn load_adc_no_well_known_path_fallback_to_mds() {
1118        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1119        let _e2 = ScopedEnv::remove("HOME"); // For posix
1120        let _e3 = ScopedEnv::remove("APPDATA"); // For windows
1121        assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
1122    }
1123
1124    #[test]
1125    #[serial_test::serial]
1126    fn load_adc_no_file_at_well_known_path_fallback_to_mds() {
1127        // Create a new temp directory. There is not an ADC file in here.
1128        let dir = tempfile::TempDir::new().unwrap();
1129        let path = dir.path().to_str().unwrap();
1130        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1131        let _e2 = ScopedEnv::set("HOME", path); // For posix
1132        let _e3 = ScopedEnv::set("APPDATA", path); // For windows
1133        assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
1134    }
1135
1136    #[test]
1137    #[serial_test::serial]
1138    fn load_adc_no_file_at_env_is_error() {
1139        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", "file-does-not-exist.json");
1140        let err = load_adc().unwrap_err();
1141        assert!(err.is_loading(), "{err:?}");
1142        let msg = format!("{err:?}");
1143        assert!(msg.contains("file-does-not-exist.json"), "{err:?}");
1144        assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"), "{err:?}");
1145    }
1146
1147    #[test]
1148    #[serial_test::serial]
1149    fn load_adc_success() {
1150        let file = tempfile::NamedTempFile::new().unwrap();
1151        let path = file.into_temp_path();
1152        std::fs::write(&path, "contents").expect("Unable to write to temporary file.");
1153        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
1154
1155        assert_eq!(
1156            load_adc().unwrap(),
1157            AdcContents::Contents("contents".to_string())
1158        );
1159    }
1160
1161    #[test_case(true; "retryable")]
1162    #[test_case(false; "non-retryable")]
1163    #[tokio::test]
1164    async fn error_credentials(retryable: bool) {
1165        let credentials = super::testing::error_credentials(retryable);
1166        assert!(
1167            credentials.universe_domain().await.is_none(),
1168            "{credentials:?}"
1169        );
1170        let err = credentials.headers(Extensions::new()).await.err().unwrap();
1171        assert_eq!(err.is_transient(), retryable, "{err:?}");
1172        let err = credentials.headers(Extensions::new()).await.err().unwrap();
1173        assert_eq!(err.is_transient(), retryable, "{err:?}");
1174    }
1175
1176    #[tokio::test]
1177    #[serial_test::serial]
1178    async fn create_access_token_credentials_fallback_to_mds_with_quota_project_override() {
1179        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1180        let _e2 = ScopedEnv::remove("HOME"); // For posix
1181        let _e3 = ScopedEnv::remove("APPDATA"); // For windows
1182        let _e4 = ScopedEnv::set(GOOGLE_CLOUD_QUOTA_PROJECT_VAR, "env-quota-project");
1183
1184        let mds = Builder::default()
1185            .with_quota_project_id("test-quota-project")
1186            .build()
1187            .unwrap();
1188        let fmt = format!("{mds:?}");
1189        assert!(fmt.contains("MDSCredentials"));
1190        assert!(
1191            fmt.contains("env-quota-project"),
1192            "Expected 'env-quota-project', got: {fmt}"
1193        );
1194    }
1195
1196    #[tokio::test]
1197    #[serial_test::serial]
1198    async fn create_access_token_credentials_with_quota_project_from_builder() {
1199        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1200        let _e2 = ScopedEnv::remove("HOME"); // For posix
1201        let _e3 = ScopedEnv::remove("APPDATA"); // For windows
1202        let _e4 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
1203
1204        let creds = Builder::default()
1205            .with_quota_project_id("test-quota-project")
1206            .build()
1207            .unwrap();
1208        let fmt = format!("{creds:?}");
1209        assert!(
1210            fmt.contains("test-quota-project"),
1211            "Expected 'test-quota-project', got: {fmt}"
1212        );
1213    }
1214
1215    #[tokio::test]
1216    #[serial_test::serial]
1217    async fn create_access_token_service_account_credentials_with_scopes() -> TestResult {
1218        let _e1 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
1219        let mut service_account_key = serde_json::json!({
1220            "type": "service_account",
1221            "project_id": "test-project-id",
1222            "private_key_id": "test-private-key-id",
1223            "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
1224            "client_email": "test-client-email",
1225            "universe_domain": "test-universe-domain"
1226        });
1227
1228        let scopes =
1229            ["https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate"];
1230
1231        service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1232
1233        let file = tempfile::NamedTempFile::new().unwrap();
1234        let path = file.into_temp_path();
1235        std::fs::write(&path, service_account_key.to_string())
1236            .expect("Unable to write to temporary file.");
1237        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
1238
1239        let sac = Builder::default()
1240            .with_quota_project_id("test-quota-project")
1241            .with_scopes(scopes)
1242            .build()
1243            .unwrap();
1244
1245        let headers = sac.headers(Extensions::new()).await?;
1246        let token = get_token_from_headers(headers).unwrap();
1247        let parts: Vec<_> = token.split('.').collect();
1248        assert_eq!(parts.len(), 3);
1249        let claims = b64_decode_to_json(parts.get(1).unwrap().to_string());
1250
1251        let fmt = format!("{sac:?}");
1252        assert!(fmt.contains("ServiceAccountCredentials"));
1253        assert!(fmt.contains("test-quota-project"));
1254        assert_eq!(claims["scope"], scopes.join(" "));
1255
1256        Ok(())
1257    }
1258
1259    #[test]
1260    fn debug_access_token() {
1261        let expires_at = Instant::now() + Duration::from_secs(3600);
1262        let token = Token {
1263            token: "token-test-only".into(),
1264            token_type: "Bearer".into(),
1265            expires_at: Some(expires_at),
1266            metadata: None,
1267        };
1268        let access_token: AccessToken = token.into();
1269        let got = format!("{access_token:?}");
1270        assert!(!got.contains("token-test-only"), "{got}");
1271        assert!(got.contains("token: \"[censored]\""), "{got}");
1272    }
1273}