Skip to main content

google_cloud_auth/
credentials.rs

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