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