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