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/// # async fn sample() -> anyhow::Result<()> {
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(()) }
420/// ```
421///
422/// [ADC-link]: https://cloud.google.com/docs/authentication/application-default-credentials
423/// [AIP-4110]: https://google.aip.dev/auth/4110
424/// [Cloud Run]: https://cloud.google.com/run
425/// [gce-link]: https://cloud.google.com/products/compute
426/// [gcloud auth application-default]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default
427/// [gke-link]: https://cloud.google.com/kubernetes-engine
428#[derive(Debug)]
429pub struct Builder {
430    quota_project_id: Option<String>,
431    scopes: Option<Vec<String>>,
432}
433
434impl Default for Builder {
435    /// Creates a new builder where credentials will be obtained via [application-default login].
436    ///
437    /// # Example
438    /// ```
439    /// # use google_cloud_auth::credentials::Builder;
440    /// # fn sample() -> anyhow::Result<()> {
441    /// let credentials = Builder::default().build()?;
442    /// # Ok(()) }
443    /// ```
444    ///
445    /// [application-default login]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login
446    fn default() -> Self {
447        Self {
448            quota_project_id: None,
449            scopes: None,
450        }
451    }
452}
453
454impl Builder {
455    /// Sets the [quota project] for these credentials.
456    ///
457    /// In some services, you can use an account in one project for authentication
458    /// and authorization, and charge the usage to a different project. This requires
459    /// that the user has `serviceusage.services.use` permissions on the quota project.
460    ///
461    /// ## Important: Precedence
462    /// If the `GOOGLE_CLOUD_QUOTA_PROJECT` environment variable is set,
463    /// its value will be used **instead of** the value provided to this method.
464    ///
465    /// # Example
466    /// ```
467    /// # use google_cloud_auth::credentials::Builder;
468    /// # fn sample() -> anyhow::Result<()> {
469    /// let credentials = Builder::default()
470    ///     .with_quota_project_id("my-project")
471    ///     .build()?;
472    /// # Ok(()) }
473    /// ```
474    ///
475    /// [quota project]: https://cloud.google.com/docs/quotas/quota-project
476    pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
477        self.quota_project_id = Some(quota_project_id.into());
478        self
479    }
480
481    /// Sets the [scopes] for these credentials.
482    ///
483    /// `scopes` act as an additional restriction in addition to the IAM permissions
484    /// granted to the principal (user or service account) that creates the token.
485    ///
486    /// `scopes` define the *permissions being requested* for this specific access token
487    /// when interacting with a service. For example,
488    /// `https://www.googleapis.com/auth/devstorage.read_write`.
489    ///
490    /// IAM permissions, on the other hand, define the *underlying capabilities*
491    /// the principal possesses within a system. For example, `storage.buckets.delete`.
492    ///
493    /// The credentials certify that a particular token was created by a certain principal.
494    ///
495    /// When a token generated with specific scopes is used, the request must be permitted
496    /// by both the the principals's underlying IAM permissions and the scopes requested
497    /// for the token.
498    ///
499    /// [scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
500    pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
501    where
502        I: IntoIterator<Item = S>,
503        S: Into<String>,
504    {
505        self.scopes = Some(scopes.into_iter().map(|s| s.into()).collect());
506        self
507    }
508
509    /// Returns a [Credentials] instance with the configured settings.
510    ///
511    /// # Errors
512    ///
513    /// Returns a [CredentialsError] if an unsupported credential type is provided
514    /// or if the JSON value is either malformed or missing required fields.
515    ///
516    /// For more information, on how to generate the JSON for a credential,
517    /// consult the relevant section in the [application-default credentials] guide.
518    ///
519    /// [application-default credentials]: https://cloud.google.com/docs/authentication/application-default-credentials
520    pub fn build(self) -> BuildResult<Credentials> {
521        Ok(self.build_access_token_credentials()?.into())
522    }
523
524    /// Returns an [AccessTokenCredentials] instance with the configured settings.
525    ///
526    /// # Example
527    ///
528    /// ```
529    /// # use google_cloud_auth::credentials::{Builder, AccessTokenCredentials, AccessTokenCredentialsProvider};
530    /// # async fn sample() -> anyhow::Result<()> {
531    /// // This will search for Application Default Credentials and build AccessTokenCredentials.
532    /// let credentials: AccessTokenCredentials = Builder::default()
533    ///     .build_access_token_credentials()?;
534    /// let access_token = credentials.access_token().await?;
535    /// println!("Token: {}", access_token.token);
536    /// # Ok(()) }
537    /// ```
538    ///
539    /// # Errors
540    ///
541    /// Returns a [CredentialsError] if an unsupported credential type is provided
542    /// or if the JSON value is either malformed or missing required fields.
543    ///
544    /// For more information, on how to generate the JSON for a credential,
545    /// consult the relevant section in the [application-default credentials] guide.
546    ///
547    /// [application-default credentials]: https://cloud.google.com/docs/authentication/application-default-credentials
548    pub fn build_access_token_credentials(self) -> BuildResult<AccessTokenCredentials> {
549        let json_data = match load_adc()? {
550            AdcContents::Contents(contents) => {
551                Some(serde_json::from_str(&contents).map_err(BuilderError::parsing)?)
552            }
553            AdcContents::FallbackToMds => None,
554        };
555        let quota_project_id = std::env::var(GOOGLE_CLOUD_QUOTA_PROJECT_VAR)
556            .ok()
557            .or(self.quota_project_id);
558        build_credentials(json_data, quota_project_id, self.scopes)
559    }
560
561    /// Returns a [crate::signer::Signer] instance with the configured settings.
562    ///
563    /// This method automatically loads Application Default Credentials (ADC)
564    /// from the environment and uses them to create a signer.
565    ///
566    /// The returned [crate::signer::Signer] might perform signing locally (e.g. if a service
567    /// account key is found) or via a remote API (e.g. if running on GCE).
568    ///
569    /// # Example
570    ///
571    /// ```
572    /// # use google_cloud_auth::credentials::Builder;
573    /// # use google_cloud_auth::signer::Signer;
574    /// # fn sample() -> anyhow::Result<()> {
575    /// let signer: Signer = Builder::default().build_signer()?;
576    /// # Ok(()) }
577    /// ```
578    pub fn build_signer(self) -> BuildResult<crate::signer::Signer> {
579        let json_data = match load_adc()? {
580            AdcContents::Contents(contents) => {
581                Some(serde_json::from_str(&contents).map_err(BuilderError::parsing)?)
582            }
583            AdcContents::FallbackToMds => None,
584        };
585        let quota_project_id = std::env::var(GOOGLE_CLOUD_QUOTA_PROJECT_VAR)
586            .ok()
587            .or(self.quota_project_id);
588        build_signer(json_data, quota_project_id, self.scopes)
589    }
590}
591
592#[derive(Debug, PartialEq)]
593enum AdcPath {
594    FromEnv(String),
595    WellKnown(String),
596}
597
598#[derive(Debug, PartialEq)]
599enum AdcContents {
600    Contents(String),
601    FallbackToMds,
602}
603
604fn extract_credential_type(json: &Value) -> BuildResult<&str> {
605    json.get("type")
606        .ok_or_else(|| BuilderError::parsing("no `type` field found."))?
607        .as_str()
608        .ok_or_else(|| BuilderError::parsing("`type` field is not a string."))
609}
610
611/// Applies common optional configurations (quota project ID, scopes) to a
612/// specific credential builder instance and then builds it.
613///
614/// This macro centralizes the logic for optionally calling `.with_quota_project_id()`
615/// and `.with_scopes()` on different underlying credential builders (like
616/// `mds::Builder`, `service_account::Builder`, etc.) before calling `.build()`.
617/// It helps avoid repetitive code in the `build_credentials` function.
618macro_rules! config_builder {
619    ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $apply_scopes_closure:expr) => {{
620        let builder = config_common_builder!(
621            $builder_instance,
622            $quota_project_id_option,
623            $scopes_option,
624            $apply_scopes_closure
625        );
626        builder.build_access_token_credentials()
627    }};
628}
629
630/// Applies common optional configurations (quota project ID, scopes) to a
631/// specific credential builder instance and then return a signer for it.
632macro_rules! config_signer {
633    ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $apply_scopes_closure:expr) => {{
634        let builder = config_common_builder!(
635            $builder_instance,
636            $quota_project_id_option,
637            $scopes_option,
638            $apply_scopes_closure
639        );
640        builder.build_signer()
641    }};
642}
643
644macro_rules! config_common_builder {
645    ($builder_instance:expr, $quota_project_id_option:expr, $scopes_option:expr, $apply_scopes_closure:expr) => {{
646        let builder = $builder_instance;
647        let builder = $quota_project_id_option
648            .into_iter()
649            .fold(builder, |b, qp| b.with_quota_project_id(qp));
650
651        let builder = $scopes_option
652            .into_iter()
653            .fold(builder, |b, s| $apply_scopes_closure(b, s));
654
655        builder
656    }};
657}
658
659fn build_credentials(
660    json: Option<Value>,
661    quota_project_id: Option<String>,
662    scopes: Option<Vec<String>>,
663) -> BuildResult<AccessTokenCredentials> {
664    match json {
665        None => config_builder!(
666            mds::Builder::from_adc(),
667            quota_project_id,
668            scopes,
669            |b: mds::Builder, s: Vec<String>| b.with_scopes(s)
670        ),
671        Some(json) => {
672            let cred_type = extract_credential_type(&json)?;
673            match cred_type {
674                "authorized_user" => {
675                    config_builder!(
676                        user_account::Builder::new(json),
677                        quota_project_id,
678                        scopes,
679                        |b: user_account::Builder, s: Vec<String>| b.with_scopes(s)
680                    )
681                }
682                "service_account" => config_builder!(
683                    service_account::Builder::new(json),
684                    quota_project_id,
685                    scopes,
686                    |b: service_account::Builder, s: Vec<String>| b
687                        .with_access_specifier(service_account::AccessSpecifier::from_scopes(s))
688                ),
689                "impersonated_service_account" => {
690                    config_builder!(
691                        impersonated::Builder::new(json),
692                        quota_project_id,
693                        scopes,
694                        |b: impersonated::Builder, s: Vec<String>| b.with_scopes(s)
695                    )
696                }
697                "external_account" => config_builder!(
698                    external_account::Builder::new(json),
699                    quota_project_id,
700                    scopes,
701                    |b: external_account::Builder, s: Vec<String>| b.with_scopes(s)
702                ),
703                _ => Err(BuilderError::unknown_type(cred_type)),
704            }
705        }
706    }
707}
708
709fn build_signer(
710    json: Option<Value>,
711    quota_project_id: Option<String>,
712    scopes: Option<Vec<String>>,
713) -> BuildResult<crate::signer::Signer> {
714    match json {
715        None => config_signer!(
716            mds::Builder::from_adc(),
717            quota_project_id,
718            scopes,
719            |b: mds::Builder, s: Vec<String>| b.with_scopes(s)
720        ),
721        Some(json) => {
722            let cred_type = extract_credential_type(&json)?;
723            match cred_type {
724                "authorized_user" => Err(BuilderError::not_supported(
725                    "authorized_user signer is not supported",
726                )),
727                "service_account" => config_signer!(
728                    service_account::Builder::new(json),
729                    quota_project_id,
730                    scopes,
731                    |b: service_account::Builder, s: Vec<String>| b
732                        .with_access_specifier(service_account::AccessSpecifier::from_scopes(s))
733                ),
734                "impersonated_service_account" => {
735                    config_signer!(
736                        impersonated::Builder::new(json),
737                        quota_project_id,
738                        scopes,
739                        |b: impersonated::Builder, s: Vec<String>| b.with_scopes(s)
740                    )
741                }
742                "external_account" => Err(BuilderError::not_supported(
743                    "external_account signer is not supported",
744                )),
745                _ => Err(BuilderError::unknown_type(cred_type)),
746            }
747        }
748    }
749}
750
751fn path_not_found(path: String) -> BuilderError {
752    BuilderError::loading(format!(
753        "{path}. {}",
754        concat!(
755            "This file name was found in the `GOOGLE_APPLICATION_CREDENTIALS` ",
756            "environment variable. Verify this environment variable points to ",
757            "a valid file."
758        )
759    ))
760}
761
762fn load_adc() -> BuildResult<AdcContents> {
763    match adc_path() {
764        None => Ok(AdcContents::FallbackToMds),
765        Some(AdcPath::FromEnv(path)) => match std::fs::read_to_string(&path) {
766            Ok(contents) => Ok(AdcContents::Contents(contents)),
767            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(path_not_found(path)),
768            Err(e) => Err(BuilderError::loading(e)),
769        },
770        Some(AdcPath::WellKnown(path)) => match std::fs::read_to_string(path) {
771            Ok(contents) => Ok(AdcContents::Contents(contents)),
772            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(AdcContents::FallbackToMds),
773            Err(e) => Err(BuilderError::loading(e)),
774        },
775    }
776}
777
778/// The path to Application Default Credentials (ADC), as specified in [AIP-4110].
779///
780/// [AIP-4110]: https://google.aip.dev/auth/4110
781fn adc_path() -> Option<AdcPath> {
782    if let Ok(path) = std::env::var("GOOGLE_APPLICATION_CREDENTIALS") {
783        return Some(AdcPath::FromEnv(path));
784    }
785    Some(AdcPath::WellKnown(adc_well_known_path()?))
786}
787
788/// The well-known path to ADC on Windows, as specified in [AIP-4113].
789///
790/// [AIP-4113]: https://google.aip.dev/auth/4113
791#[cfg(target_os = "windows")]
792fn adc_well_known_path() -> Option<String> {
793    std::env::var("APPDATA")
794        .ok()
795        .map(|root| root + "/gcloud/application_default_credentials.json")
796}
797
798/// The well-known path to ADC on Linux and Mac, as specified in [AIP-4113].
799///
800/// [AIP-4113]: https://google.aip.dev/auth/4113
801#[cfg(not(target_os = "windows"))]
802fn adc_well_known_path() -> Option<String> {
803    std::env::var("HOME")
804        .ok()
805        .map(|root| root + "/.config/gcloud/application_default_credentials.json")
806}
807
808/// A module providing invalid credentials where authentication does not matter.
809///
810/// These credentials are a convenient way to avoid errors from loading
811/// Application Default Credentials in tests.
812///
813/// This module is mainly relevant to other `google-cloud-*` crates, but some
814/// external developers (i.e. consumers, not developers of `google-cloud-rust`)
815/// may find it useful.
816// Skipping mutation testing for this module. As it exclusively provides
817// hardcoded credential stubs for testing purposes.
818#[cfg_attr(test, mutants::skip)]
819#[doc(hidden)]
820pub mod testing {
821    use super::CacheableResource;
822    use crate::Result;
823    use crate::credentials::Credentials;
824    use crate::credentials::dynamic::CredentialsProvider;
825    use http::{Extensions, HeaderMap};
826    use std::sync::Arc;
827
828    /// A simple credentials implementation to use in tests.
829    ///
830    /// Always return an error in `headers()`.
831    pub fn error_credentials(retryable: bool) -> Credentials {
832        Credentials {
833            inner: Arc::from(ErrorCredentials(retryable)),
834        }
835    }
836
837    #[derive(Debug, Default)]
838    struct ErrorCredentials(bool);
839
840    #[async_trait::async_trait]
841    impl CredentialsProvider for ErrorCredentials {
842        async fn headers(&self, _extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
843            Err(super::CredentialsError::from_msg(self.0, "test-only"))
844        }
845
846        async fn universe_domain(&self) -> Option<String> {
847            None
848        }
849    }
850}
851
852#[cfg(test)]
853pub(crate) mod tests {
854    use super::*;
855    use crate::constants::TRUST_BOUNDARY_HEADER;
856    use base64::Engine;
857    use google_cloud_gax::backoff_policy::BackoffPolicy;
858    use google_cloud_gax::retry_policy::RetryPolicy;
859    use google_cloud_gax::retry_result::RetryResult;
860    use google_cloud_gax::retry_state::RetryState;
861    use google_cloud_gax::retry_throttler::RetryThrottler;
862    use mockall::mock;
863    use reqwest::header::AUTHORIZATION;
864    use rsa::BigUint;
865    use rsa::RsaPrivateKey;
866    use rsa::pkcs8::{EncodePrivateKey, LineEnding};
867    use scoped_env::ScopedEnv;
868    use std::error::Error;
869    use std::sync::LazyLock;
870    use test_case::test_case;
871    use tokio::time::Duration;
872    use tokio::time::Instant;
873
874    pub(crate) fn find_source_error<'a, T: Error + 'static>(
875        error: &'a (dyn Error + 'static),
876    ) -> Option<&'a T> {
877        let mut source = error.source();
878        while let Some(err) = source {
879            if let Some(target_err) = err.downcast_ref::<T>() {
880                return Some(target_err);
881            }
882            source = err.source();
883        }
884        None
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_transient = error
928                    .source()
929                    .and_then(|e| e.downcast_ref::<CredentialsError>())
930                    .is_some_and(|ce| ce.is_transient());
931                if is_transient {
932                    RetryResult::Continue(error)
933                } else {
934                    RetryResult::Permanent(error)
935                }
936            });
937        retry_policy
938    }
939
940    pub(crate) fn get_mock_backoff_policy() -> MockBackoffPolicy {
941        let mut backoff_policy = MockBackoffPolicy::new();
942        backoff_policy
943            .expect_on_failure()
944            .return_const(Duration::from_secs(0));
945        backoff_policy
946    }
947
948    pub(crate) fn get_mock_retry_throttler() -> MockRetryThrottler {
949        let mut throttler = MockRetryThrottler::new();
950        throttler.expect_on_retry_failure().return_const(());
951        throttler
952            .expect_throttle_retry_attempt()
953            .return_const(false);
954        throttler.expect_on_success().return_const(());
955        throttler
956    }
957
958    pub(crate) fn get_headers_from_cache(
959        headers: CacheableResource<HeaderMap>,
960    ) -> Result<HeaderMap> {
961        match headers {
962            CacheableResource::New { data, .. } => Ok(data),
963            CacheableResource::NotModified => Err(CredentialsError::from_msg(
964                false,
965                "Expecting headers to be present",
966            )),
967        }
968    }
969
970    pub(crate) fn get_token_from_headers(headers: CacheableResource<HeaderMap>) -> Option<String> {
971        match headers {
972            CacheableResource::New { data, .. } => data
973                .get(AUTHORIZATION)
974                .and_then(|token_value| token_value.to_str().ok())
975                .and_then(|s| s.split_whitespace().nth(1))
976                .map(|s| s.to_string()),
977            CacheableResource::NotModified => None,
978        }
979    }
980
981    pub(crate) fn get_access_boundary_from_headers(
982        headers: CacheableResource<HeaderMap>,
983    ) -> Option<String> {
984        match headers {
985            CacheableResource::New { data, .. } => data
986                .get(TRUST_BOUNDARY_HEADER)
987                .and_then(|token_value| token_value.to_str().ok())
988                .map(|s| s.to_string()),
989            CacheableResource::NotModified => None,
990        }
991    }
992
993    pub(crate) fn get_token_type_from_headers(
994        headers: CacheableResource<HeaderMap>,
995    ) -> Option<String> {
996        match headers {
997            CacheableResource::New { data, .. } => data
998                .get(AUTHORIZATION)
999                .and_then(|token_value| token_value.to_str().ok())
1000                .and_then(|s| s.split_whitespace().next())
1001                .map(|s| s.to_string()),
1002            CacheableResource::NotModified => None,
1003        }
1004    }
1005
1006    pub static RSA_PRIVATE_KEY: LazyLock<RsaPrivateKey> = LazyLock::new(|| {
1007        let p_str: &str = "141367881524527794394893355677826002829869068195396267579403819572502936761383874443619453704612633353803671595972343528718438130450055151198231345212263093247511629886734453413988207866331439612464122904648042654465604881130663408340669956544709445155137282157402427763452856646879397237752891502149781819597";
1008        let q_str: &str = "179395413952110013801471600075409598322058038890563483332288896635704255883613060744402506322679437982046475766067250097809676406576067239936945362857700460740092421061356861438909617220234758121022105150630083703531219941303688818533566528599328339894969707615478438750812672509434761181735933851075292740309";
1009        let e_str: &str = "65537";
1010
1011        let p = BigUint::parse_bytes(p_str.as_bytes(), 10).expect("Failed to parse prime P");
1012        let q = BigUint::parse_bytes(q_str.as_bytes(), 10).expect("Failed to parse prime Q");
1013        let public_exponent =
1014            BigUint::parse_bytes(e_str.as_bytes(), 10).expect("Failed to parse public exponent");
1015
1016        RsaPrivateKey::from_primes(vec![p, q], public_exponent)
1017            .expect("Failed to create RsaPrivateKey from primes")
1018    });
1019
1020    #[cfg(feature = "idtoken")]
1021    pub static ES256_PRIVATE_KEY: LazyLock<p256::SecretKey> = LazyLock::new(|| {
1022        let secret_key_bytes = [
1023            0x4c, 0x0c, 0x11, 0x6e, 0x6e, 0xb0, 0x07, 0xbd, 0x48, 0x0c, 0xc0, 0x48, 0xc0, 0x1f,
1024            0xac, 0x3d, 0x82, 0x82, 0x0e, 0x6c, 0x3d, 0x76, 0x61, 0x4d, 0x06, 0x4e, 0xdb, 0x05,
1025            0x26, 0x6c, 0x75, 0xdf,
1026        ];
1027        p256::SecretKey::from_bytes((&secret_key_bytes).into()).unwrap()
1028    });
1029
1030    pub static PKCS8_PK: LazyLock<String> = LazyLock::new(|| {
1031        RSA_PRIVATE_KEY
1032            .to_pkcs8_pem(LineEnding::LF)
1033            .expect("Failed to encode key to PKCS#8 PEM")
1034            .to_string()
1035    });
1036
1037    pub fn b64_decode_to_json(s: String) -> serde_json::Value {
1038        let decoded = String::from_utf8(
1039            base64::engine::general_purpose::URL_SAFE_NO_PAD
1040                .decode(s)
1041                .unwrap(),
1042        )
1043        .unwrap();
1044        serde_json::from_str(&decoded).unwrap()
1045    }
1046
1047    #[cfg(target_os = "windows")]
1048    #[test]
1049    #[serial_test::serial]
1050    fn adc_well_known_path_windows() {
1051        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1052        let _appdata = ScopedEnv::set("APPDATA", "C:/Users/foo");
1053        assert_eq!(
1054            adc_well_known_path(),
1055            Some("C:/Users/foo/gcloud/application_default_credentials.json".to_string())
1056        );
1057        assert_eq!(
1058            adc_path(),
1059            Some(AdcPath::WellKnown(
1060                "C:/Users/foo/gcloud/application_default_credentials.json".to_string()
1061            ))
1062        );
1063    }
1064
1065    #[cfg(target_os = "windows")]
1066    #[test]
1067    #[serial_test::serial]
1068    fn adc_well_known_path_windows_no_appdata() {
1069        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1070        let _appdata = ScopedEnv::remove("APPDATA");
1071        assert_eq!(adc_well_known_path(), None);
1072        assert_eq!(adc_path(), None);
1073    }
1074
1075    #[cfg(not(target_os = "windows"))]
1076    #[test]
1077    #[serial_test::serial]
1078    fn adc_well_known_path_posix() {
1079        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1080        let _home = ScopedEnv::set("HOME", "/home/foo");
1081        assert_eq!(
1082            adc_well_known_path(),
1083            Some("/home/foo/.config/gcloud/application_default_credentials.json".to_string())
1084        );
1085        assert_eq!(
1086            adc_path(),
1087            Some(AdcPath::WellKnown(
1088                "/home/foo/.config/gcloud/application_default_credentials.json".to_string()
1089            ))
1090        );
1091    }
1092
1093    #[cfg(not(target_os = "windows"))]
1094    #[test]
1095    #[serial_test::serial]
1096    fn adc_well_known_path_posix_no_home() {
1097        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1098        let _appdata = ScopedEnv::remove("HOME");
1099        assert_eq!(adc_well_known_path(), None);
1100        assert_eq!(adc_path(), None);
1101    }
1102
1103    #[test]
1104    #[serial_test::serial]
1105    fn adc_path_from_env() {
1106        let _creds = ScopedEnv::set(
1107            "GOOGLE_APPLICATION_CREDENTIALS",
1108            "/usr/bar/application_default_credentials.json",
1109        );
1110        assert_eq!(
1111            adc_path(),
1112            Some(AdcPath::FromEnv(
1113                "/usr/bar/application_default_credentials.json".to_string()
1114            ))
1115        );
1116    }
1117
1118    #[test]
1119    #[serial_test::serial]
1120    fn load_adc_no_well_known_path_fallback_to_mds() {
1121        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1122        let _e2 = ScopedEnv::remove("HOME"); // For posix
1123        let _e3 = ScopedEnv::remove("APPDATA"); // For windows
1124        assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
1125    }
1126
1127    #[test]
1128    #[serial_test::serial]
1129    fn load_adc_no_file_at_well_known_path_fallback_to_mds() {
1130        // Create a new temp directory. There is not an ADC file in here.
1131        let dir = tempfile::TempDir::new().unwrap();
1132        let path = dir.path().to_str().unwrap();
1133        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1134        let _e2 = ScopedEnv::set("HOME", path); // For posix
1135        let _e3 = ScopedEnv::set("APPDATA", path); // For windows
1136        assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
1137    }
1138
1139    #[test]
1140    #[serial_test::serial]
1141    fn load_adc_no_file_at_env_is_error() {
1142        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", "file-does-not-exist.json");
1143        let err = load_adc().unwrap_err();
1144        assert!(err.is_loading(), "{err:?}");
1145        let msg = format!("{err:?}");
1146        assert!(msg.contains("file-does-not-exist.json"), "{err:?}");
1147        assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"), "{err:?}");
1148    }
1149
1150    #[test]
1151    #[serial_test::serial]
1152    fn load_adc_success() {
1153        let file = tempfile::NamedTempFile::new().unwrap();
1154        let path = file.into_temp_path();
1155        std::fs::write(&path, "contents").expect("Unable to write to temporary file.");
1156        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
1157
1158        assert_eq!(
1159            load_adc().unwrap(),
1160            AdcContents::Contents("contents".to_string())
1161        );
1162    }
1163
1164    #[test_case(true; "retryable")]
1165    #[test_case(false; "non-retryable")]
1166    #[tokio::test]
1167    async fn error_credentials(retryable: bool) {
1168        let credentials = super::testing::error_credentials(retryable);
1169        assert!(
1170            credentials.universe_domain().await.is_none(),
1171            "{credentials:?}"
1172        );
1173        let err = credentials.headers(Extensions::new()).await.err().unwrap();
1174        assert_eq!(err.is_transient(), retryable, "{err:?}");
1175        let err = credentials.headers(Extensions::new()).await.err().unwrap();
1176        assert_eq!(err.is_transient(), retryable, "{err:?}");
1177    }
1178
1179    #[tokio::test]
1180    #[serial_test::serial]
1181    async fn create_access_token_credentials_fallback_to_mds_with_quota_project_override() {
1182        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1183        let _e2 = ScopedEnv::remove("HOME"); // For posix
1184        let _e3 = ScopedEnv::remove("APPDATA"); // For windows
1185        let _e4 = ScopedEnv::set(GOOGLE_CLOUD_QUOTA_PROJECT_VAR, "env-quota-project");
1186
1187        let mds = Builder::default()
1188            .with_quota_project_id("test-quota-project")
1189            .build()
1190            .unwrap();
1191        let fmt = format!("{mds:?}");
1192        assert!(fmt.contains("MDSCredentials"));
1193        assert!(
1194            fmt.contains("env-quota-project"),
1195            "Expected 'env-quota-project', got: {fmt}"
1196        );
1197    }
1198
1199    #[tokio::test]
1200    #[serial_test::serial]
1201    async fn create_access_token_credentials_with_quota_project_from_builder() {
1202        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1203        let _e2 = ScopedEnv::remove("HOME"); // For posix
1204        let _e3 = ScopedEnv::remove("APPDATA"); // For windows
1205        let _e4 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
1206
1207        let creds = Builder::default()
1208            .with_quota_project_id("test-quota-project")
1209            .build()
1210            .unwrap();
1211        let fmt = format!("{creds:?}");
1212        assert!(
1213            fmt.contains("test-quota-project"),
1214            "Expected 'test-quota-project', got: {fmt}"
1215        );
1216    }
1217
1218    #[tokio::test]
1219    #[serial_test::serial]
1220    async fn create_access_token_service_account_credentials_with_scopes() -> TestResult {
1221        let _e1 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
1222        let mut service_account_key = serde_json::json!({
1223            "type": "service_account",
1224            "project_id": "test-project-id",
1225            "private_key_id": "test-private-key-id",
1226            "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
1227            "client_email": "test-client-email",
1228            "universe_domain": "test-universe-domain"
1229        });
1230
1231        let scopes =
1232            ["https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate"];
1233
1234        service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1235
1236        let file = tempfile::NamedTempFile::new().unwrap();
1237        let path = file.into_temp_path();
1238        std::fs::write(&path, service_account_key.to_string())
1239            .expect("Unable to write to temporary file.");
1240        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
1241
1242        let sac = Builder::default()
1243            .with_quota_project_id("test-quota-project")
1244            .with_scopes(scopes)
1245            .build()
1246            .unwrap();
1247
1248        let headers = sac.headers(Extensions::new()).await?;
1249        let token = get_token_from_headers(headers).unwrap();
1250        let parts: Vec<_> = token.split('.').collect();
1251        assert_eq!(parts.len(), 3);
1252        let claims = b64_decode_to_json(parts.get(1).unwrap().to_string());
1253
1254        let fmt = format!("{sac:?}");
1255        assert!(fmt.contains("ServiceAccountCredentials"));
1256        assert!(fmt.contains("test-quota-project"));
1257        assert_eq!(claims["scope"], scopes.join(" "));
1258
1259        Ok(())
1260    }
1261
1262    #[test]
1263    fn debug_access_token() {
1264        let expires_at = Instant::now() + Duration::from_secs(3600);
1265        let token = Token {
1266            token: "token-test-only".into(),
1267            token_type: "Bearer".into(),
1268            expires_at: Some(expires_at),
1269            metadata: None,
1270        };
1271        let access_token: AccessToken = token.into();
1272        let got = format!("{access_token:?}");
1273        assert!(!got.contains("token-test-only"), "{got}");
1274        assert!(got.contains("token: \"[censored]\""), "{got}");
1275    }
1276}