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    // `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    // `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 base64::Engine;
859    use gax::backoff_policy::BackoffPolicy;
860    use gax::retry_policy::RetryPolicy;
861    use gax::retry_result::RetryResult;
862    use gax::retry_state::RetryState;
863    use gax::retry_throttler::RetryThrottler;
864    use mockall::mock;
865    use reqwest::header::AUTHORIZATION;
866    use rsa::BigUint;
867    use rsa::RsaPrivateKey;
868    use rsa::pkcs8::{EncodePrivateKey, LineEnding};
869    use scoped_env::ScopedEnv;
870    use std::error::Error;
871    use std::sync::LazyLock;
872    use test_case::test_case;
873    use tokio::time::Duration;
874    use tokio::time::Instant;
875
876    pub(crate) fn find_source_error<'a, T: Error + 'static>(
877        error: &'a (dyn Error + 'static),
878    ) -> Option<&'a T> {
879        let mut source = error.source();
880        while let Some(err) = source {
881            if let Some(target_err) = err.downcast_ref::<T>() {
882                return Some(target_err);
883            }
884            source = err.source();
885        }
886        None
887    }
888
889    mock! {
890        #[derive(Debug)]
891        pub RetryPolicy {}
892        impl RetryPolicy for RetryPolicy {
893            fn on_error(
894                &self,
895                state: &RetryState,
896                error: gax::error::Error,
897            ) -> RetryResult;
898        }
899    }
900
901    mock! {
902        #[derive(Debug)]
903        pub BackoffPolicy {}
904        impl BackoffPolicy for BackoffPolicy {
905            fn on_failure(&self, state: &RetryState) -> std::time::Duration;
906        }
907    }
908
909    mockall::mock! {
910        #[derive(Debug)]
911        pub RetryThrottler {}
912        impl RetryThrottler for RetryThrottler {
913            fn throttle_retry_attempt(&self) -> bool;
914            fn on_retry_failure(&mut self, error: &RetryResult);
915            fn on_success(&mut self);
916        }
917    }
918
919    type TestResult = std::result::Result<(), Box<dyn std::error::Error>>;
920
921    pub(crate) fn get_mock_auth_retry_policy(attempts: usize) -> MockRetryPolicy {
922        let mut retry_policy = MockRetryPolicy::new();
923        retry_policy
924            .expect_on_error()
925            .returning(move |state, error| {
926                if state.attempt_count >= attempts as u32 {
927                    return RetryResult::Exhausted(error);
928                }
929                let is_transient = error
930                    .source()
931                    .and_then(|e| e.downcast_ref::<CredentialsError>())
932                    .is_some_and(|ce| ce.is_transient());
933                if is_transient {
934                    RetryResult::Continue(error)
935                } else {
936                    RetryResult::Permanent(error)
937                }
938            });
939        retry_policy
940    }
941
942    pub(crate) fn get_mock_backoff_policy() -> MockBackoffPolicy {
943        let mut backoff_policy = MockBackoffPolicy::new();
944        backoff_policy
945            .expect_on_failure()
946            .return_const(Duration::from_secs(0));
947        backoff_policy
948    }
949
950    pub(crate) fn get_mock_retry_throttler() -> MockRetryThrottler {
951        let mut throttler = MockRetryThrottler::new();
952        throttler.expect_on_retry_failure().return_const(());
953        throttler
954            .expect_throttle_retry_attempt()
955            .return_const(false);
956        throttler.expect_on_success().return_const(());
957        throttler
958    }
959
960    pub(crate) fn get_headers_from_cache(
961        headers: CacheableResource<HeaderMap>,
962    ) -> Result<HeaderMap> {
963        match headers {
964            CacheableResource::New { data, .. } => Ok(data),
965            CacheableResource::NotModified => Err(CredentialsError::from_msg(
966                false,
967                "Expecting headers to be present",
968            )),
969        }
970    }
971
972    pub(crate) fn get_token_from_headers(headers: CacheableResource<HeaderMap>) -> Option<String> {
973        match headers {
974            CacheableResource::New { data, .. } => data
975                .get(AUTHORIZATION)
976                .and_then(|token_value| token_value.to_str().ok())
977                .and_then(|s| s.split_whitespace().nth(1))
978                .map(|s| s.to_string()),
979            CacheableResource::NotModified => None,
980        }
981    }
982
983    pub(crate) fn get_token_type_from_headers(
984        headers: CacheableResource<HeaderMap>,
985    ) -> Option<String> {
986        match headers {
987            CacheableResource::New { data, .. } => data
988                .get(AUTHORIZATION)
989                .and_then(|token_value| token_value.to_str().ok())
990                .and_then(|s| s.split_whitespace().next())
991                .map(|s| s.to_string()),
992            CacheableResource::NotModified => None,
993        }
994    }
995
996    pub static RSA_PRIVATE_KEY: LazyLock<RsaPrivateKey> = LazyLock::new(|| {
997        let p_str: &str = "141367881524527794394893355677826002829869068195396267579403819572502936761383874443619453704612633353803671595972343528718438130450055151198231345212263093247511629886734453413988207866331439612464122904648042654465604881130663408340669956544709445155137282157402427763452856646879397237752891502149781819597";
998        let q_str: &str = "179395413952110013801471600075409598322058038890563483332288896635704255883613060744402506322679437982046475766067250097809676406576067239936945362857700460740092421061356861438909617220234758121022105150630083703531219941303688818533566528599328339894969707615478438750812672509434761181735933851075292740309";
999        let e_str: &str = "65537";
1000
1001        let p = BigUint::parse_bytes(p_str.as_bytes(), 10).expect("Failed to parse prime P");
1002        let q = BigUint::parse_bytes(q_str.as_bytes(), 10).expect("Failed to parse prime Q");
1003        let public_exponent =
1004            BigUint::parse_bytes(e_str.as_bytes(), 10).expect("Failed to parse public exponent");
1005
1006        RsaPrivateKey::from_primes(vec![p, q], public_exponent)
1007            .expect("Failed to create RsaPrivateKey from primes")
1008    });
1009
1010    pub static ES256_PRIVATE_KEY: LazyLock<p256::SecretKey> = LazyLock::new(|| {
1011        let secret_key_bytes = [
1012            0x4c, 0x0c, 0x11, 0x6e, 0x6e, 0xb0, 0x07, 0xbd, 0x48, 0x0c, 0xc0, 0x48, 0xc0, 0x1f,
1013            0xac, 0x3d, 0x82, 0x82, 0x0e, 0x6c, 0x3d, 0x76, 0x61, 0x4d, 0x06, 0x4e, 0xdb, 0x05,
1014            0x26, 0x6c, 0x75, 0xdf,
1015        ];
1016        p256::SecretKey::from_bytes((&secret_key_bytes).into()).unwrap()
1017    });
1018
1019    pub static PKCS8_PK: LazyLock<String> = LazyLock::new(|| {
1020        RSA_PRIVATE_KEY
1021            .to_pkcs8_pem(LineEnding::LF)
1022            .expect("Failed to encode key to PKCS#8 PEM")
1023            .to_string()
1024    });
1025
1026    pub fn b64_decode_to_json(s: String) -> serde_json::Value {
1027        let decoded = String::from_utf8(
1028            base64::engine::general_purpose::URL_SAFE_NO_PAD
1029                .decode(s)
1030                .unwrap(),
1031        )
1032        .unwrap();
1033        serde_json::from_str(&decoded).unwrap()
1034    }
1035
1036    #[cfg(target_os = "windows")]
1037    #[test]
1038    #[serial_test::serial]
1039    fn adc_well_known_path_windows() {
1040        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1041        let _appdata = ScopedEnv::set("APPDATA", "C:/Users/foo");
1042        assert_eq!(
1043            adc_well_known_path(),
1044            Some("C:/Users/foo/gcloud/application_default_credentials.json".to_string())
1045        );
1046        assert_eq!(
1047            adc_path(),
1048            Some(AdcPath::WellKnown(
1049                "C:/Users/foo/gcloud/application_default_credentials.json".to_string()
1050            ))
1051        );
1052    }
1053
1054    #[cfg(target_os = "windows")]
1055    #[test]
1056    #[serial_test::serial]
1057    fn adc_well_known_path_windows_no_appdata() {
1058        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1059        let _appdata = ScopedEnv::remove("APPDATA");
1060        assert_eq!(adc_well_known_path(), None);
1061        assert_eq!(adc_path(), None);
1062    }
1063
1064    #[cfg(not(target_os = "windows"))]
1065    #[test]
1066    #[serial_test::serial]
1067    fn adc_well_known_path_posix() {
1068        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1069        let _home = ScopedEnv::set("HOME", "/home/foo");
1070        assert_eq!(
1071            adc_well_known_path(),
1072            Some("/home/foo/.config/gcloud/application_default_credentials.json".to_string())
1073        );
1074        assert_eq!(
1075            adc_path(),
1076            Some(AdcPath::WellKnown(
1077                "/home/foo/.config/gcloud/application_default_credentials.json".to_string()
1078            ))
1079        );
1080    }
1081
1082    #[cfg(not(target_os = "windows"))]
1083    #[test]
1084    #[serial_test::serial]
1085    fn adc_well_known_path_posix_no_home() {
1086        let _creds = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1087        let _appdata = ScopedEnv::remove("HOME");
1088        assert_eq!(adc_well_known_path(), None);
1089        assert_eq!(adc_path(), None);
1090    }
1091
1092    #[test]
1093    #[serial_test::serial]
1094    fn adc_path_from_env() {
1095        let _creds = ScopedEnv::set(
1096            "GOOGLE_APPLICATION_CREDENTIALS",
1097            "/usr/bar/application_default_credentials.json",
1098        );
1099        assert_eq!(
1100            adc_path(),
1101            Some(AdcPath::FromEnv(
1102                "/usr/bar/application_default_credentials.json".to_string()
1103            ))
1104        );
1105    }
1106
1107    #[test]
1108    #[serial_test::serial]
1109    fn load_adc_no_well_known_path_fallback_to_mds() {
1110        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1111        let _e2 = ScopedEnv::remove("HOME"); // For posix
1112        let _e3 = ScopedEnv::remove("APPDATA"); // For windows
1113        assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
1114    }
1115
1116    #[test]
1117    #[serial_test::serial]
1118    fn load_adc_no_file_at_well_known_path_fallback_to_mds() {
1119        // Create a new temp directory. There is not an ADC file in here.
1120        let dir = tempfile::TempDir::new().unwrap();
1121        let path = dir.path().to_str().unwrap();
1122        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1123        let _e2 = ScopedEnv::set("HOME", path); // For posix
1124        let _e3 = ScopedEnv::set("APPDATA", path); // For windows
1125        assert_eq!(load_adc().unwrap(), AdcContents::FallbackToMds);
1126    }
1127
1128    #[test]
1129    #[serial_test::serial]
1130    fn load_adc_no_file_at_env_is_error() {
1131        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", "file-does-not-exist.json");
1132        let err = load_adc().unwrap_err();
1133        assert!(err.is_loading(), "{err:?}");
1134        let msg = format!("{err:?}");
1135        assert!(msg.contains("file-does-not-exist.json"), "{err:?}");
1136        assert!(msg.contains("GOOGLE_APPLICATION_CREDENTIALS"), "{err:?}");
1137    }
1138
1139    #[test]
1140    #[serial_test::serial]
1141    fn load_adc_success() {
1142        let file = tempfile::NamedTempFile::new().unwrap();
1143        let path = file.into_temp_path();
1144        std::fs::write(&path, "contents").expect("Unable to write to temporary file.");
1145        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
1146
1147        assert_eq!(
1148            load_adc().unwrap(),
1149            AdcContents::Contents("contents".to_string())
1150        );
1151    }
1152
1153    #[test_case(true; "retryable")]
1154    #[test_case(false; "non-retryable")]
1155    #[tokio::test]
1156    async fn error_credentials(retryable: bool) {
1157        let credentials = super::testing::error_credentials(retryable);
1158        assert!(
1159            credentials.universe_domain().await.is_none(),
1160            "{credentials:?}"
1161        );
1162        let err = credentials.headers(Extensions::new()).await.err().unwrap();
1163        assert_eq!(err.is_transient(), retryable, "{err:?}");
1164        let err = credentials.headers(Extensions::new()).await.err().unwrap();
1165        assert_eq!(err.is_transient(), retryable, "{err:?}");
1166    }
1167
1168    #[tokio::test]
1169    #[serial_test::serial]
1170    async fn create_access_token_credentials_fallback_to_mds_with_quota_project_override() {
1171        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1172        let _e2 = ScopedEnv::remove("HOME"); // For posix
1173        let _e3 = ScopedEnv::remove("APPDATA"); // For windows
1174        let _e4 = ScopedEnv::set(GOOGLE_CLOUD_QUOTA_PROJECT_VAR, "env-quota-project");
1175
1176        let mds = Builder::default()
1177            .with_quota_project_id("test-quota-project")
1178            .build()
1179            .unwrap();
1180        let fmt = format!("{mds:?}");
1181        assert!(fmt.contains("MDSCredentials"));
1182        assert!(
1183            fmt.contains("env-quota-project"),
1184            "Expected 'env-quota-project', got: {fmt}"
1185        );
1186    }
1187
1188    #[tokio::test]
1189    #[serial_test::serial]
1190    async fn create_access_token_credentials_with_quota_project_from_builder() {
1191        let _e1 = ScopedEnv::remove("GOOGLE_APPLICATION_CREDENTIALS");
1192        let _e2 = ScopedEnv::remove("HOME"); // For posix
1193        let _e3 = ScopedEnv::remove("APPDATA"); // For windows
1194        let _e4 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
1195
1196        let creds = Builder::default()
1197            .with_quota_project_id("test-quota-project")
1198            .build()
1199            .unwrap();
1200        let fmt = format!("{creds:?}");
1201        assert!(
1202            fmt.contains("test-quota-project"),
1203            "Expected 'test-quota-project', got: {fmt}"
1204        );
1205    }
1206
1207    #[tokio::test]
1208    #[serial_test::serial]
1209    async fn create_access_token_service_account_credentials_with_scopes() -> TestResult {
1210        let _e1 = ScopedEnv::remove(GOOGLE_CLOUD_QUOTA_PROJECT_VAR);
1211        let mut service_account_key = serde_json::json!({
1212            "type": "service_account",
1213            "project_id": "test-project-id",
1214            "private_key_id": "test-private-key-id",
1215            "private_key": "-----BEGIN PRIVATE KEY-----\nBLAHBLAHBLAH\n-----END PRIVATE KEY-----\n",
1216            "client_email": "test-client-email",
1217            "universe_domain": "test-universe-domain"
1218        });
1219
1220        let scopes =
1221            ["https://www.googleapis.com/auth/pubsub, https://www.googleapis.com/auth/translate"];
1222
1223        service_account_key["private_key"] = Value::from(PKCS8_PK.clone());
1224
1225        let file = tempfile::NamedTempFile::new().unwrap();
1226        let path = file.into_temp_path();
1227        std::fs::write(&path, service_account_key.to_string())
1228            .expect("Unable to write to temporary file.");
1229        let _e = ScopedEnv::set("GOOGLE_APPLICATION_CREDENTIALS", path.to_str().unwrap());
1230
1231        let sac = Builder::default()
1232            .with_quota_project_id("test-quota-project")
1233            .with_scopes(scopes)
1234            .build()
1235            .unwrap();
1236
1237        let headers = sac.headers(Extensions::new()).await?;
1238        let token = get_token_from_headers(headers).unwrap();
1239        let parts: Vec<_> = token.split('.').collect();
1240        assert_eq!(parts.len(), 3);
1241        let claims = b64_decode_to_json(parts.get(1).unwrap().to_string());
1242
1243        let fmt = format!("{sac:?}");
1244        assert!(fmt.contains("ServiceAccountCredentials"));
1245        assert!(fmt.contains("test-quota-project"));
1246        assert_eq!(claims["scope"], scopes.join(" "));
1247
1248        Ok(())
1249    }
1250
1251    #[test]
1252    fn debug_access_token() {
1253        let expires_at = Instant::now() + Duration::from_secs(3600);
1254        let token = Token {
1255            token: "token-test-only".into(),
1256            token_type: "Bearer".into(),
1257            expires_at: Some(expires_at),
1258            metadata: None,
1259        };
1260        let access_token: AccessToken = token.into();
1261        let got = format!("{access_token:?}");
1262        assert!(!got.contains("token-test-only"), "{got}");
1263        assert!(got.contains("token: \"[censored]\""), "{got}");
1264    }
1265}