Skip to main content

google_cloud_auth/credentials/
impersonated.rs

1// Copyright 2025 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//! [Impersonated service account] credentials.
16//!
17//! When the principal you are using doesn't have the permissions you need to
18//! accomplish your task, or you want to use a service account in a development
19//! environment, you can use service account impersonation. The typical principals
20//! used to impersonate a service account are [User Account] or another [Service Account].
21//!
22//! The principal that is trying to impersonate a target service account should have
23//! [Service Account Token Creator Role] on the target service account.
24//!
25//! ## Example: Creating credentials from a JSON object
26//!
27//! ```
28//! # use google_cloud_auth::credentials::impersonated;
29//! # use serde_json::json;
30//! # use std::time::Duration;
31//! # use http::Extensions;
32//! #
33//! # tokio_test::block_on(async {
34//! let source_credentials = json!({
35//!     "type": "authorized_user",
36//!     "client_id": "test-client-id",
37//!     "client_secret": "test-client-secret",
38//!     "refresh_token": "test-refresh-token"
39//! });
40//!
41//! let impersonated_credential = json!({
42//!     "type": "impersonated_service_account",
43//!     "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
44//!     "source_credentials": source_credentials,
45//! });
46//!
47//! let credentials = impersonated::Builder::new(impersonated_credential)
48//!     .with_lifetime(Duration::from_secs(500))
49//!     .build()?;
50//! let headers = credentials.headers(Extensions::new()).await?;
51//! println!("Headers: {headers:?}");
52//! # Ok::<(), anyhow::Error>(())
53//! # });
54//! ```
55//!
56//! ## Example: Creating credentials with custom retry behavior
57//!
58//! ```
59//! # use google_cloud_auth::credentials::impersonated;
60//! # use serde_json::json;
61//! # use std::time::Duration;
62//! # use http::Extensions;
63//! # tokio_test::block_on(async {
64//! use gax::retry_policy::{AlwaysRetry, RetryPolicyExt};
65//! use gax::exponential_backoff::ExponentialBackoff;
66//! # let source_credentials = json!({
67//! #     "type": "authorized_user",
68//! #     "client_id": "test-client-id",
69//! #     "client_secret": "test-client-secret",
70//! #     "refresh_token": "test-refresh-token"
71//! # });
72//! #
73//! # let impersonated_credential = json!({
74//! #     "type": "impersonated_service_account",
75//! #     "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
76//! #     "source_credentials": source_credentials,
77//! # });
78//! let backoff = ExponentialBackoff::default();
79//! let credentials = impersonated::Builder::new(impersonated_credential)
80//!     .with_retry_policy(AlwaysRetry.with_attempt_limit(3))
81//!     .with_backoff_policy(backoff)
82//!     .build()?;
83//! let headers = credentials.headers(Extensions::new()).await?;
84//! println!("Headers: {headers:?}");
85//! # Ok::<(), anyhow::Error>(())
86//! # });
87//! ```
88//!
89//! [Impersonated service account]: https://cloud.google.com/docs/authentication/use-service-account-impersonation
90//! [User Account]: https://cloud.google.com/docs/authentication#user-accounts
91//! [Service Account]: https://cloud.google.com/iam/docs/service-account-overview
92//! [Service Account Token Creator Role]: https://cloud.google.com/docs/authentication/use-service-account-impersonation#required-roles
93
94use crate::build_errors::Error as BuilderError;
95use crate::constants::DEFAULT_SCOPE;
96use crate::credentials::dynamic::{AccessTokenCredentialsProvider, CredentialsProvider};
97use crate::credentials::{
98    AccessToken, AccessTokenCredentials, CacheableResource, Credentials, build_credentials,
99    extract_credential_type,
100};
101use crate::errors::{self, CredentialsError};
102use crate::headers_util::{
103    self, ACCESS_TOKEN_REQUEST_TYPE, build_cacheable_headers, metrics_header_value,
104};
105use crate::retry::{Builder as RetryTokenProviderBuilder, TokenProviderWithRetry};
106use crate::token::{CachedTokenProvider, Token, TokenProvider};
107use crate::token_cache::TokenCache;
108use crate::{BuildResult, Result};
109use async_trait::async_trait;
110use gax::backoff_policy::BackoffPolicyArg;
111use gax::retry_policy::RetryPolicyArg;
112use gax::retry_throttler::RetryThrottlerArg;
113use http::{Extensions, HeaderMap};
114use reqwest::Client;
115use serde_json::Value;
116use std::fmt::Debug;
117use std::sync::Arc;
118use std::time::Duration;
119use time::OffsetDateTime;
120use tokio::time::Instant;
121
122pub(crate) const IMPERSONATED_CREDENTIAL_TYPE: &str = "imp";
123pub(crate) const DEFAULT_LIFETIME: Duration = Duration::from_secs(3600);
124pub(crate) const MSG: &str = "failed to fetch token";
125
126#[derive(Debug, Clone)]
127pub(crate) enum BuilderSource {
128    FromJson(Value),
129    FromCredentials(Credentials),
130}
131
132/// A builder for constructing Impersonated Service Account [Credentials] instance.
133///
134/// # Example
135/// ```
136/// # use google_cloud_auth::credentials::impersonated::Builder;
137/// # tokio_test::block_on(async {
138/// let impersonated_credential = serde_json::json!({
139///     "type": "impersonated_service_account",
140///     "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
141///     "source_credentials": {
142///         "type": "authorized_user",
143///         "client_id": "test-client-id",
144///         "client_secret": "test-client-secret",
145///         "refresh_token": "test-refresh-token"
146///     }
147/// });
148/// let credentials = Builder::new(impersonated_credential).build();
149/// # });
150/// ```
151pub struct Builder {
152    source: BuilderSource,
153    service_account_impersonation_url: Option<String>,
154    delegates: Option<Vec<String>>,
155    scopes: Option<Vec<String>>,
156    quota_project_id: Option<String>,
157    lifetime: Option<Duration>,
158    retry_builder: RetryTokenProviderBuilder,
159}
160
161impl Builder {
162    /// Creates a new builder using `impersonated_service_account` JSON value.
163    ///
164    /// The `impersonated_service_account` JSON is typically generated using
165    /// a [gcloud command] for [application default login].
166    ///
167    /// [gcloud command]: https://cloud.google.com/docs/authentication/use-service-account-impersonation#adc
168    /// [application default login]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login
169    pub fn new(impersonated_credential: Value) -> Self {
170        Self {
171            source: BuilderSource::FromJson(impersonated_credential),
172            service_account_impersonation_url: None,
173            delegates: None,
174            scopes: None,
175            quota_project_id: None,
176            lifetime: None,
177            retry_builder: RetryTokenProviderBuilder::default(),
178        }
179    }
180
181    /// Creates a new builder with a source credentials object.
182    ///
183    /// # Example
184    /// ```
185    /// # use google_cloud_auth::credentials::impersonated;
186    /// # use google_cloud_auth::credentials::user_account;
187    /// # use serde_json::json;
188    /// #
189    /// # tokio_test::block_on(async {
190    /// let source_credentials = user_account::Builder::new(json!({ /* add details here */ })).build()?;
191    ///
192    /// let creds = impersonated::Builder::from_source_credentials(source_credentials)
193    ///     .with_target_principal("test-principal")
194    ///     .build()?;
195    /// # Ok::<(), anyhow::Error>(())
196    /// # });
197    /// ```
198    pub fn from_source_credentials(source_credentials: Credentials) -> Self {
199        Self {
200            source: BuilderSource::FromCredentials(source_credentials),
201            service_account_impersonation_url: None,
202            delegates: None,
203            scopes: None,
204            quota_project_id: None,
205            lifetime: None,
206            retry_builder: RetryTokenProviderBuilder::default(),
207        }
208    }
209
210    /// Sets the target principal. This is required when using `from_source_credentials`.
211    /// Target principal is the email of the service account to impersonate.
212    ///
213    /// # Example
214    /// ```
215    /// # use google_cloud_auth::credentials::impersonated;
216    /// # use serde_json::json;
217    /// #
218    /// # tokio_test::block_on(async {
219    /// let impersonated_credential = json!({ /* add details here */ });
220    ///
221    /// let creds = impersonated::Builder::new(impersonated_credential.into())
222    ///     .with_target_principal("test-principal")
223    ///     .build();
224    /// # });
225    /// ```
226    pub fn with_target_principal<S: Into<String>>(mut self, target_principal: S) -> Self {
227        self.service_account_impersonation_url = Some(format!(
228            "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{}:generateAccessToken",
229            target_principal.into()
230        ));
231        self
232    }
233
234    /// Sets the chain of delegates.
235    ///
236    /// # Example
237    /// ```
238    /// # use google_cloud_auth::credentials::impersonated;
239    /// # use serde_json::json;
240    /// #
241    /// # tokio_test::block_on(async {
242    /// let impersonated_credential = json!({ /* add details here */ });
243    ///
244    /// let creds = impersonated::Builder::new(impersonated_credential.into())
245    ///     .with_delegates(["delegate1", "delegate2"])
246    ///     .build();
247    /// # });
248    /// ```
249    pub fn with_delegates<I, S>(mut self, delegates: I) -> Self
250    where
251        I: IntoIterator<Item = S>,
252        S: Into<String>,
253    {
254        self.delegates = Some(delegates.into_iter().map(|s| s.into()).collect());
255        self
256    }
257
258    /// Sets the [scopes] for these credentials.
259    ///
260    /// Any value set here overrides a `scopes` value from the
261    /// input `impersonated_service_account` JSON.
262    ///
263    /// By default `https://www.googleapis.com/auth/cloud-platform` scope is used.
264    ///
265    /// # Example
266    /// ```
267    /// # use google_cloud_auth::credentials::impersonated;
268    /// # use serde_json::json;
269    /// #
270    /// # tokio_test::block_on(async {
271    /// let impersonated_credential = json!({ /* add details here */ });
272    ///
273    /// let creds = impersonated::Builder::new(impersonated_credential.into())
274    ///     .with_scopes(["https://www.googleapis.com/auth/pubsub"])
275    ///     .build();
276    /// # });
277    /// ```
278    /// [scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
279    pub fn with_scopes<I, S>(mut self, scopes: I) -> Self
280    where
281        I: IntoIterator<Item = S>,
282        S: Into<String>,
283    {
284        self.scopes = Some(scopes.into_iter().map(|s| s.into()).collect());
285        self
286    }
287
288    /// Sets the [quota project] for these credentials.
289    ///
290    /// For some services, you can use an account in
291    /// one project for authentication and authorization, and charge
292    /// the usage to a different project. This requires that the
293    /// target service account has `serviceusage.services.use`
294    /// permissions on the quota project.
295    ///
296    /// Any value set here overrides a `quota_project_id` value from the
297    /// input `impersonated_service_account` JSON.
298    ///
299    /// # Example
300    /// ```
301    /// # use google_cloud_auth::credentials::impersonated;
302    /// # use serde_json::json;
303    /// #
304    /// # tokio_test::block_on(async {
305    /// let impersonated_credential = json!({ /* add details here */ });
306    ///
307    /// let creds = impersonated::Builder::new(impersonated_credential.into())
308    ///     .with_quota_project_id("my-project")
309    ///     .build();
310    /// # });
311    /// ```
312    /// [quota project]: https://cloud.google.com/docs/quotas/quota-project
313    pub fn with_quota_project_id<S: Into<String>>(mut self, quota_project_id: S) -> Self {
314        self.quota_project_id = Some(quota_project_id.into());
315        self
316    }
317
318    /// Sets the lifetime for the impersonated credentials.
319    ///
320    /// # Example
321    /// ```
322    /// # use google_cloud_auth::credentials::impersonated;
323    /// # use serde_json::json;
324    /// # use std::time::Duration;
325    /// #
326    /// # tokio_test::block_on(async {
327    /// let impersonated_credential = json!({ /* add details here */ });
328    ///
329    /// let creds = impersonated::Builder::new(impersonated_credential.into())
330    ///     .with_lifetime(Duration::from_secs(500))
331    ///     .build();
332    /// # });
333    /// ```
334    pub fn with_lifetime(mut self, lifetime: Duration) -> Self {
335        self.lifetime = Some(lifetime);
336        self
337    }
338
339    /// Configure the retry policy for fetching tokens.
340    ///
341    /// The retry policy controls how to handle retries, and sets limits on
342    /// the number of attempts or the total time spent retrying.
343    ///
344    /// ```
345    /// # use google_cloud_auth::credentials::impersonated::Builder;
346    /// # use serde_json::json;
347    /// # tokio_test::block_on(async {
348    /// use gax::retry_policy::{AlwaysRetry, RetryPolicyExt};
349    /// let impersonated_credential = json!({ /* add details here */ });
350    /// let credentials = Builder::new(impersonated_credential.into())
351    ///     .with_retry_policy(AlwaysRetry.with_attempt_limit(3))
352    ///     .build();
353    /// # });
354    /// ```
355    pub fn with_retry_policy<V: Into<RetryPolicyArg>>(mut self, v: V) -> Self {
356        self.retry_builder = self.retry_builder.with_retry_policy(v.into());
357        self
358    }
359
360    /// Configure the retry backoff policy.
361    ///
362    /// The backoff policy controls how long to wait in between retry attempts.
363    ///
364    /// ```
365    /// # use google_cloud_auth::credentials::impersonated::Builder;
366    /// # use serde_json::json;
367    /// # use std::time::Duration;
368    /// # tokio_test::block_on(async {
369    /// use gax::exponential_backoff::ExponentialBackoff;
370    /// let policy = ExponentialBackoff::default();
371    /// let impersonated_credential = json!({ /* add details here */ });
372    /// let credentials = Builder::new(impersonated_credential.into())
373    ///     .with_backoff_policy(policy)
374    ///     .build();
375    /// # });
376    /// ```
377    pub fn with_backoff_policy<V: Into<BackoffPolicyArg>>(mut self, v: V) -> Self {
378        self.retry_builder = self.retry_builder.with_backoff_policy(v.into());
379        self
380    }
381
382    /// Configure the retry throttler.
383    ///
384    /// Advanced applications may want to configure a retry throttler to
385    /// [Address Cascading Failures] and when [Handling Overload] conditions.
386    /// The authentication library throttles its retry loop, using a policy to
387    /// control the throttling algorithm. Use this method to fine tune or
388    /// customize the default retry throttler.
389    ///
390    /// [Handling Overload]: https://sre.google/sre-book/handling-overload/
391    /// [Address Cascading Failures]: https://sre.google/sre-book/addressing-cascading-failures/
392    ///
393    /// ```
394    /// # use google_cloud_auth::credentials::impersonated::Builder;
395    /// # use serde_json::json;
396    /// # tokio_test::block_on(async {
397    /// use gax::retry_throttler::AdaptiveThrottler;
398    /// let impersonated_credential = json!({ /* add details here */ });
399    /// let credentials = Builder::new(impersonated_credential.into())
400    ///     .with_retry_throttler(AdaptiveThrottler::default())
401    ///     .build();
402    /// # });
403    /// ```
404    pub fn with_retry_throttler<V: Into<RetryThrottlerArg>>(mut self, v: V) -> Self {
405        self.retry_builder = self.retry_builder.with_retry_throttler(v.into());
406        self
407    }
408
409    /// Returns a [Credentials] instance with the configured settings.
410    ///
411    /// # Errors
412    ///
413    /// Returns a [BuilderError] for one of the following cases:
414    /// - If the `impersonated_service_account` provided to [`Builder::new`] cannot
415    ///   be successfully deserialized into the expected format. This typically happens
416    ///   if the JSON value is malformed or missing required fields. For more information,
417    ///   on how to generate `impersonated_service_account` json, consult the relevant
418    ///   section in the [application-default credentials] guide.
419    /// - If the `impersonated_service_account` provided to [`Builder::new`] has a
420    ///   `source_credentials` of `impersonated_service_account` type.
421    /// - If `service_account_impersonation_url` is not provided after initializing
422    ///   the builder with [`Builder::from_source_credentials`].
423    ///
424    /// [application-default credentials]: https://cloud.google.com/docs/authentication/application-default-credentials
425    pub fn build(self) -> BuildResult<Credentials> {
426        Ok(self.build_access_token_credentials()?.into())
427    }
428
429    /// Returns an [AccessTokenCredentials] instance with the configured settings.
430    ///
431    /// # Example
432    ///
433    /// ```
434    /// # use google_cloud_auth::credentials::impersonated::Builder;
435    /// # use google_cloud_auth::credentials::{AccessTokenCredentials, AccessTokenCredentialsProvider};
436    /// # use serde_json::json;
437    /// # tokio_test::block_on(async {
438    /// let impersonated_credential = json!({
439    ///     "type": "impersonated_service_account",
440    ///     "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
441    ///     "source_credentials": {
442    ///         "type": "authorized_user",
443    ///         "client_id": "test-client-id",
444    ///         "client_secret": "test-client-secret",
445    ///         "refresh_token": "test-refresh-token"
446    ///     }
447    /// });
448    /// let credentials: AccessTokenCredentials = Builder::new(impersonated_credential.into())
449    ///     .build_access_token_credentials()?;
450    /// let access_token = credentials.access_token().await?;
451    /// println!("Token: {}", access_token.token);
452    /// # Ok::<(), anyhow::Error>(())
453    /// # });
454    /// ```
455    ///
456    /// # Errors
457    ///
458    /// Returns a [BuilderError] for one of the following cases:
459    /// - If the `impersonated_service_account` provided to [`Builder::new`] cannot
460    ///   be successfully deserialized into the expected format. This typically happens
461    ///   if the JSON value is malformed or missing required fields. For more information,
462    ///   on how to generate `impersonated_service_account` json, consult the relevant
463    ///   section in the [application-default credentials] guide.
464    /// - If the `impersonated_service_account` provided to [`Builder::new`] has a
465    ///   `source_credentials` of `impersonated_service_account` type.
466    /// - If `service_account_impersonation_url` is not provided after initializing
467    ///   the builder with [`Builder::from_source_credentials`].
468    ///
469    /// [application-default credentials]: https://cloud.google.com/docs/authentication/application-default-credentials
470    pub fn build_access_token_credentials(self) -> BuildResult<AccessTokenCredentials> {
471        let (token_provider, quota_project_id) = self.build_components()?;
472        Ok(AccessTokenCredentials {
473            inner: Arc::new(ImpersonatedServiceAccount {
474                token_provider: TokenCache::new(token_provider),
475                quota_project_id,
476            }),
477        })
478    }
479
480    /// Returns a [crate::signer::Signer] instance with the configured settings.
481    ///
482    /// The returned [crate::signer::Signer] uses the [IAM signBlob API] to sign content. This API
483    /// requires a network request for each signing operation.
484    ///
485    /// Note that if the source credentials are a service account, the signer
486    /// might perform signing locally. For more details on local signing, see
487    /// [crate::credentials::service_account::Builder::build_signer].
488    ///
489    /// # Example
490    ///
491    /// ```
492    /// # use google_cloud_auth::credentials::impersonated::Builder;
493    /// # use google_cloud_auth::signer::Signer;
494    /// # use serde_json::json;
495    /// # tokio_test::block_on(async {
496    /// let impersonated_credential = json!({
497    ///     "type": "impersonated_service_account",
498    ///     "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
499    ///     "source_credentials": {
500    ///         "type": "authorized_user",
501    ///         "client_id": "test-client-id",
502    ///         "client_secret": "test-client-secret",
503    ///         "refresh_token": "test-refresh-token"
504    ///     }
505    /// });
506    ///
507    /// let signer: Signer = Builder::new(impersonated_credential).build_signer()?;
508    /// # Ok::<(), anyhow::Error>(())
509    /// # });
510    /// ```
511    ///
512    /// [IAM signBlob API]: https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/signBlob
513    pub fn build_signer(self) -> BuildResult<crate::signer::Signer> {
514        self.build_signer_with_iam_endpoint_override(None)
515    }
516
517    // only used for testing
518    fn build_signer_with_iam_endpoint_override(
519        self,
520        iam_endpoint: Option<String>,
521    ) -> BuildResult<crate::signer::Signer> {
522        let source = self.source.clone();
523        match source {
524            BuilderSource::FromJson(json) => {
525                let signer = build_signer_from_json(json.clone())?;
526                if let Some(signer) = signer {
527                    return Ok(signer);
528                }
529                let components = build_components_from_json(json)?;
530                let client_email =
531                    extract_client_email(&components.service_account_impersonation_url)?;
532                let creds = self.build()?;
533                let signer = crate::signer::iam::IamSigner::new(client_email, creds, iam_endpoint);
534                Ok(crate::signer::Signer {
535                    inner: Arc::new(signer),
536                })
537            }
538            BuilderSource::FromCredentials(source_credentials) => {
539                let components = build_components_from_credentials(
540                    source_credentials,
541                    self.service_account_impersonation_url.clone(),
542                )?;
543                let client_email =
544                    extract_client_email(&components.service_account_impersonation_url)?;
545                let creds = self.build()?;
546                let signer = crate::signer::iam::IamSigner::new(client_email, creds, iam_endpoint);
547                Ok(crate::signer::Signer {
548                    inner: Arc::new(signer),
549                })
550            }
551        }
552    }
553
554    fn build_components(
555        self,
556    ) -> BuildResult<(
557        TokenProviderWithRetry<ImpersonatedTokenProvider>,
558        Option<String>,
559    )> {
560        let components = match self.source {
561            BuilderSource::FromJson(json) => build_components_from_json(json)?,
562            BuilderSource::FromCredentials(source_credentials) => {
563                build_components_from_credentials(
564                    source_credentials,
565                    self.service_account_impersonation_url,
566                )?
567            }
568        };
569
570        let scopes = self
571            .scopes
572            .or(components.scopes)
573            .unwrap_or_else(|| vec![DEFAULT_SCOPE.to_string()]);
574
575        let quota_project_id = self.quota_project_id.or(components.quota_project_id);
576        let delegates = self.delegates.or(components.delegates);
577
578        let token_provider = ImpersonatedTokenProvider {
579            source_credentials: components.source_credentials,
580            service_account_impersonation_url: components.service_account_impersonation_url,
581            delegates,
582            scopes,
583            lifetime: self.lifetime.unwrap_or(DEFAULT_LIFETIME),
584        };
585        let token_provider = self.retry_builder.build(token_provider);
586        Ok((token_provider, quota_project_id))
587    }
588}
589
590pub(crate) struct ImpersonatedCredentialComponents {
591    pub(crate) source_credentials: Credentials,
592    pub(crate) service_account_impersonation_url: String,
593    pub(crate) delegates: Option<Vec<String>>,
594    pub(crate) quota_project_id: Option<String>,
595    pub(crate) scopes: Option<Vec<String>>,
596}
597
598pub(crate) fn build_components_from_json(
599    json: Value,
600) -> BuildResult<ImpersonatedCredentialComponents> {
601    let config =
602        serde_json::from_value::<ImpersonatedConfig>(json).map_err(BuilderError::parsing)?;
603
604    let source_credential_type = extract_credential_type(&config.source_credentials)?;
605    if source_credential_type == "impersonated_service_account" {
606        return Err(BuilderError::parsing(
607            "source credential of type `impersonated_service_account` is not supported. \
608                        Use the `delegates` field to specify a delegation chain.",
609        ));
610    }
611
612    // Do not pass along scopes and quota project to the source credentials.
613    // It is not necessary that the source and target credentials have same permissions on
614    // the quota project and they typically need different scopes.
615    // If user does want some specific scopes or quota, they can build using the
616    // from_source_credentials method.
617    let source_credentials = build_credentials(Some(config.source_credentials), None, None)?.into();
618
619    Ok(ImpersonatedCredentialComponents {
620        source_credentials,
621        service_account_impersonation_url: config.service_account_impersonation_url,
622        delegates: config.delegates,
623        quota_project_id: config.quota_project_id,
624        scopes: config.scopes,
625    })
626}
627
628// Check if the source credentials is a service account and if so, build a signer from it.
629// Service account signers can do signing locally, which is more efficient than using the
630// remote/API based signer.
631fn build_signer_from_json(json: Value) -> BuildResult<Option<crate::signer::Signer>> {
632    use crate::credentials::service_account::ServiceAccountKey;
633    use crate::signer::service_account::ServiceAccountSigner;
634
635    let config =
636        serde_json::from_value::<ImpersonatedConfig>(json).map_err(BuilderError::parsing)?;
637
638    let client_email = extract_client_email(&config.service_account_impersonation_url)?;
639    let source_credential_type = extract_credential_type(&config.source_credentials)?;
640    if source_credential_type == "service_account" {
641        let service_account_key =
642            serde_json::from_value::<ServiceAccountKey>(config.source_credentials)
643                .map_err(BuilderError::parsing)?;
644        let signing_provider = ServiceAccountSigner::from_impersonated_service_account(
645            service_account_key,
646            client_email,
647        );
648        let signer = crate::signer::Signer {
649            inner: Arc::new(signing_provider),
650        };
651        return Ok(Some(signer));
652    }
653    Ok(None)
654}
655
656fn extract_client_email(service_account_impersonation_url: &str) -> BuildResult<String> {
657    let mut parts = service_account_impersonation_url.split("/serviceAccounts/");
658    match (parts.nth(1), parts.next()) {
659        (Some(email), None) => Ok(email.trim_end_matches(":generateAccessToken").to_string()),
660        _ => Err(BuilderError::parsing(
661            "invalid service account impersonation URL",
662        )),
663    }
664}
665
666pub(crate) fn build_components_from_credentials(
667    source_credentials: Credentials,
668    service_account_impersonation_url: Option<String>,
669) -> BuildResult<ImpersonatedCredentialComponents> {
670    let url = service_account_impersonation_url.ok_or_else(|| {
671        BuilderError::parsing(
672            "`service_account_impersonation_url` is required when building from source credentials",
673        )
674    })?;
675    Ok(ImpersonatedCredentialComponents {
676        source_credentials,
677        service_account_impersonation_url: url,
678        delegates: None,
679        quota_project_id: None,
680        scopes: None,
681    })
682}
683
684#[derive(serde::Deserialize, Debug, PartialEq)]
685struct ImpersonatedConfig {
686    service_account_impersonation_url: String,
687    source_credentials: Value,
688    delegates: Option<Vec<String>>,
689    quota_project_id: Option<String>,
690    scopes: Option<Vec<String>>,
691}
692
693#[derive(Debug)]
694struct ImpersonatedServiceAccount<T>
695where
696    T: CachedTokenProvider,
697{
698    token_provider: T,
699    quota_project_id: Option<String>,
700}
701
702#[async_trait::async_trait]
703impl<T> CredentialsProvider for ImpersonatedServiceAccount<T>
704where
705    T: CachedTokenProvider,
706{
707    async fn headers(&self, extensions: Extensions) -> Result<CacheableResource<HeaderMap>> {
708        let token = self.token_provider.token(extensions).await?;
709        build_cacheable_headers(&token, &self.quota_project_id)
710    }
711}
712
713#[async_trait::async_trait]
714impl<T> AccessTokenCredentialsProvider for ImpersonatedServiceAccount<T>
715where
716    T: CachedTokenProvider,
717{
718    async fn access_token(&self) -> Result<AccessToken> {
719        let token = self.token_provider.token(Extensions::new()).await?;
720        token.into()
721    }
722}
723
724struct ImpersonatedTokenProvider {
725    source_credentials: Credentials,
726    service_account_impersonation_url: String,
727    delegates: Option<Vec<String>>,
728    scopes: Vec<String>,
729    lifetime: Duration,
730}
731
732impl Debug for ImpersonatedTokenProvider {
733    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
734        f.debug_struct("ImpersonatedTokenProvider")
735            .field("source_credentials", &self.source_credentials)
736            .field(
737                "service_account_impersonation_url",
738                &self.service_account_impersonation_url,
739            )
740            .field("delegates", &self.delegates)
741            .field("scopes", &self.scopes)
742            .field("lifetime", &self.lifetime)
743            .finish()
744    }
745}
746
747#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)]
748struct GenerateAccessTokenRequest {
749    #[serde(skip_serializing_if = "Option::is_none")]
750    delegates: Option<Vec<String>>,
751    scope: Vec<String>,
752    lifetime: String,
753}
754
755pub(crate) async fn generate_access_token(
756    source_headers: HeaderMap,
757    delegates: Option<Vec<String>>,
758    scopes: Vec<String>,
759    lifetime: Duration,
760    service_account_impersonation_url: &str,
761) -> Result<Token> {
762    let client = Client::new();
763    let body = GenerateAccessTokenRequest {
764        delegates,
765        scope: scopes,
766        lifetime: format!("{}s", lifetime.as_secs_f64()),
767    };
768
769    let response = client
770        .post(service_account_impersonation_url)
771        .header("Content-Type", "application/json")
772        .header(
773            headers_util::X_GOOG_API_CLIENT,
774            metrics_header_value(ACCESS_TOKEN_REQUEST_TYPE, IMPERSONATED_CREDENTIAL_TYPE),
775        )
776        .headers(source_headers)
777        .json(&body)
778        .send()
779        .await
780        .map_err(|e| errors::from_http_error(e, MSG))?;
781
782    if !response.status().is_success() {
783        let err = errors::from_http_response(response, MSG).await;
784        return Err(err);
785    }
786
787    let token_response = response
788        .json::<GenerateAccessTokenResponse>()
789        .await
790        .map_err(|e| {
791            let retryable = !e.is_decode();
792            CredentialsError::from_source(retryable, e)
793        })?;
794
795    let parsed_dt = OffsetDateTime::parse(
796        &token_response.expire_time,
797        &time::format_description::well_known::Rfc3339,
798    )
799    .map_err(errors::non_retryable)?;
800
801    let remaining_duration = parsed_dt - OffsetDateTime::now_utc();
802    let expires_at = Instant::now() + remaining_duration.try_into().unwrap();
803
804    let token = Token {
805        token: token_response.access_token,
806        token_type: "Bearer".to_string(),
807        expires_at: Some(expires_at),
808        metadata: None,
809    };
810    Ok(token)
811}
812
813#[async_trait]
814impl TokenProvider for ImpersonatedTokenProvider {
815    async fn token(&self) -> Result<Token> {
816        let source_headers = self.source_credentials.headers(Extensions::new()).await?;
817        let source_headers = match source_headers {
818            CacheableResource::New { data, .. } => data,
819            CacheableResource::NotModified => {
820                unreachable!("requested source credentials without a caching etag")
821            }
822        };
823        generate_access_token(
824            source_headers,
825            self.delegates.clone(),
826            self.scopes.clone(),
827            self.lifetime,
828            &self.service_account_impersonation_url,
829        )
830        .await
831    }
832}
833
834#[derive(serde::Deserialize)]
835struct GenerateAccessTokenResponse {
836    #[serde(rename = "accessToken")]
837    access_token: String,
838    #[serde(rename = "expireTime")]
839    expire_time: String,
840}
841
842#[cfg(test)]
843mod tests {
844    use super::*;
845    use crate::credentials::tests::{
846        find_source_error, get_mock_auth_retry_policy, get_mock_backoff_policy,
847        get_mock_retry_throttler,
848    };
849    use crate::errors::CredentialsError;
850    use httptest::cycle;
851    use httptest::{Expectation, Server, matchers::*, responders::*};
852    use serde_json::json;
853
854    type TestResult = anyhow::Result<()>;
855
856    #[tokio::test]
857    async fn test_generate_access_token_success() -> TestResult {
858        let server = Server::run();
859        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
860            .format(&time::format_description::well_known::Rfc3339)
861            .unwrap();
862        server.expect(
863            Expectation::matching(all_of![
864                request::method_path(
865                    "POST",
866                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
867                ),
868                request::headers(contains(("authorization", "Bearer test-token"))),
869            ])
870            .respond_with(json_encoded(json!({
871                "accessToken": "test-impersonated-token",
872                "expireTime": expire_time
873            }))),
874        );
875
876        let mut headers = HeaderMap::new();
877        headers.insert("authorization", "Bearer test-token".parse().unwrap());
878        let token = generate_access_token(
879            headers,
880            None,
881            vec!["scope".to_string()],
882            DEFAULT_LIFETIME,
883            &server
884                .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
885                .to_string(),
886        )
887        .await?;
888
889        assert_eq!(token.token, "test-impersonated-token");
890        Ok(())
891    }
892
893    #[tokio::test]
894    async fn test_generate_access_token_403() -> TestResult {
895        let server = Server::run();
896        server.expect(
897            Expectation::matching(all_of![
898                request::method_path(
899                    "POST",
900                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
901                ),
902                request::headers(contains(("authorization", "Bearer test-token"))),
903            ])
904            .respond_with(status_code(403)),
905        );
906
907        let mut headers = HeaderMap::new();
908        headers.insert("authorization", "Bearer test-token".parse().unwrap());
909        let err = generate_access_token(
910            headers,
911            None,
912            vec!["scope".to_string()],
913            DEFAULT_LIFETIME,
914            &server
915                .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
916                .to_string(),
917        )
918        .await
919        .unwrap_err();
920
921        assert!(!err.is_transient());
922        Ok(())
923    }
924
925    #[tokio::test]
926    async fn test_generate_access_token_no_auth_header() -> TestResult {
927        let server = Server::run();
928        server.expect(
929            Expectation::matching(request::method_path(
930                "POST",
931                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
932            ))
933            .respond_with(status_code(401)),
934        );
935
936        let err = generate_access_token(
937            HeaderMap::new(),
938            None,
939            vec!["scope".to_string()],
940            DEFAULT_LIFETIME,
941            &server
942                .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
943                .to_string(),
944        )
945        .await
946        .unwrap_err();
947
948        assert!(!err.is_transient());
949        Ok(())
950    }
951
952    #[tokio::test]
953    async fn test_impersonated_service_account() -> TestResult {
954        let server = Server::run();
955        server.expect(
956            Expectation::matching(request::method_path("POST", "/token")).respond_with(
957                json_encoded(json!({
958                    "access_token": "test-user-account-token",
959                    "expires_in": 3600,
960                    "token_type": "Bearer",
961                })),
962            ),
963        );
964        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
965            .format(&time::format_description::well_known::Rfc3339)
966            .unwrap();
967        server.expect(
968            Expectation::matching(all_of![
969                request::method_path(
970                    "POST",
971                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
972                ),
973                request::headers(contains((
974                    "authorization",
975                    "Bearer test-user-account-token"
976                ))),
977                request::body(json_decoded(eq(json!({
978                    "scope": ["scope1", "scope2"],
979                    "lifetime": "3600s"
980                }))))
981            ])
982            .respond_with(json_encoded(json!({
983                "accessToken": "test-impersonated-token",
984                "expireTime": expire_time
985            }))),
986        );
987
988        let impersonated_credential = json!({
989            "type": "impersonated_service_account",
990            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
991            "source_credentials": {
992                "type": "authorized_user",
993                "client_id": "test-client-id",
994                "client_secret": "test-client-secret",
995                "refresh_token": "test-refresh-token",
996                "token_uri": server.url("/token").to_string()
997            }
998        });
999        let (token_provider, _) = Builder::new(impersonated_credential)
1000            .with_scopes(vec!["scope1", "scope2"])
1001            .build_components()?;
1002
1003        let token = token_provider.token().await?;
1004        assert_eq!(token.token, "test-impersonated-token");
1005        assert_eq!(token.token_type, "Bearer");
1006
1007        Ok(())
1008    }
1009
1010    #[tokio::test]
1011    async fn test_impersonated_service_account_default_scope() -> TestResult {
1012        let server = Server::run();
1013        server.expect(
1014            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1015                json_encoded(json!({
1016                    "access_token": "test-user-account-token",
1017                    "expires_in": 3600,
1018                    "token_type": "Bearer",
1019                })),
1020            ),
1021        );
1022        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1023            .format(&time::format_description::well_known::Rfc3339)
1024            .unwrap();
1025        server.expect(
1026            Expectation::matching(all_of![
1027                request::method_path(
1028                    "POST",
1029                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1030                ),
1031                request::headers(contains((
1032                    "authorization",
1033                    "Bearer test-user-account-token"
1034                ))),
1035                request::body(json_decoded(eq(json!({
1036                    "scope": [DEFAULT_SCOPE],
1037                    "lifetime": "3600s"
1038                }))))
1039            ])
1040            .respond_with(json_encoded(json!({
1041                "accessToken": "test-impersonated-token",
1042                "expireTime": expire_time
1043            }))),
1044        );
1045
1046        let impersonated_credential = json!({
1047            "type": "impersonated_service_account",
1048            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1049            "source_credentials": {
1050                "type": "authorized_user",
1051                "client_id": "test-client-id",
1052                "client_secret": "test-client-secret",
1053                "refresh_token": "test-refresh-token",
1054                "token_uri": server.url("/token").to_string()
1055            }
1056        });
1057        let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1058
1059        let token = token_provider.token().await?;
1060        assert_eq!(token.token, "test-impersonated-token");
1061        assert_eq!(token.token_type, "Bearer");
1062
1063        Ok(())
1064    }
1065
1066    #[tokio::test]
1067    async fn test_impersonated_service_account_with_custom_lifetime() -> TestResult {
1068        let server = Server::run();
1069        server.expect(
1070            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1071                json_encoded(json!({
1072                    "access_token": "test-user-account-token",
1073                    "expires_in": 3600,
1074                    "token_type": "Bearer",
1075                })),
1076            ),
1077        );
1078        let expire_time = (OffsetDateTime::now_utc() + time::Duration::seconds(500))
1079            .format(&time::format_description::well_known::Rfc3339)
1080            .unwrap();
1081        server.expect(
1082            Expectation::matching(all_of![
1083                request::method_path(
1084                    "POST",
1085                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1086                ),
1087                request::headers(contains((
1088                    "authorization",
1089                    "Bearer test-user-account-token"
1090                ))),
1091                request::body(json_decoded(eq(json!({
1092                    "scope": ["scope1", "scope2"],
1093                    "lifetime": "3.5s"
1094                }))))
1095            ])
1096            .respond_with(json_encoded(json!({
1097                "accessToken": "test-impersonated-token",
1098                "expireTime": expire_time
1099            }))),
1100        );
1101
1102        let impersonated_credential = json!({
1103            "type": "impersonated_service_account",
1104            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1105            "source_credentials": {
1106                "type": "authorized_user",
1107                "client_id": "test-client-id",
1108                "client_secret": "test-client-secret",
1109                "refresh_token": "test-refresh-token",
1110                "token_uri": server.url("/token").to_string()
1111            }
1112        });
1113        let (token_provider, _) = Builder::new(impersonated_credential)
1114            .with_scopes(vec!["scope1", "scope2"])
1115            .with_lifetime(Duration::from_secs_f32(3.5))
1116            .build_components()?;
1117
1118        let token = token_provider.token().await?;
1119        assert_eq!(token.token, "test-impersonated-token");
1120
1121        Ok(())
1122    }
1123
1124    #[tokio::test]
1125    async fn test_with_delegates() -> TestResult {
1126        let server = Server::run();
1127        server.expect(
1128            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1129                json_encoded(json!({
1130                    "access_token": "test-user-account-token",
1131                    "expires_in": 3600,
1132                    "token_type": "Bearer",
1133                })),
1134            ),
1135        );
1136        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1137            .format(&time::format_description::well_known::Rfc3339)
1138            .unwrap();
1139        server.expect(
1140            Expectation::matching(all_of![
1141                request::method_path(
1142                    "POST",
1143                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1144                ),
1145                request::headers(contains((
1146                    "authorization",
1147                    "Bearer test-user-account-token"
1148                ))),
1149                request::body(json_decoded(eq(json!({
1150                    "scope": [DEFAULT_SCOPE],
1151                    "lifetime": "3600s",
1152                    "delegates": ["delegate1", "delegate2"]
1153                }))))
1154            ])
1155            .respond_with(json_encoded(json!({
1156                "accessToken": "test-impersonated-token",
1157                "expireTime": expire_time
1158            }))),
1159        );
1160
1161        let impersonated_credential = json!({
1162            "type": "impersonated_service_account",
1163            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1164            "source_credentials": {
1165                "type": "authorized_user",
1166                "client_id": "test-client-id",
1167                "client_secret": "test-client-secret",
1168                "refresh_token": "test-refresh-token",
1169                "token_uri": server.url("/token").to_string()
1170            }
1171        });
1172        let (token_provider, _) = Builder::new(impersonated_credential)
1173            .with_delegates(vec!["delegate1", "delegate2"])
1174            .build_components()?;
1175
1176        let token = token_provider.token().await?;
1177        assert_eq!(token.token, "test-impersonated-token");
1178        assert_eq!(token.token_type, "Bearer");
1179
1180        Ok(())
1181    }
1182
1183    #[tokio::test]
1184    async fn test_impersonated_service_account_fail() -> TestResult {
1185        let server = Server::run();
1186        server.expect(
1187            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1188                json_encoded(json!({
1189                    "access_token": "test-user-account-token",
1190                    "expires_in": 3600,
1191                    "token_type": "Bearer",
1192                })),
1193            ),
1194        );
1195        server.expect(
1196            Expectation::matching(request::method_path(
1197                "POST",
1198                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1199            ))
1200            .respond_with(status_code(500)),
1201        );
1202
1203        let impersonated_credential = json!({
1204            "type": "impersonated_service_account",
1205            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1206            "source_credentials": {
1207                "type": "authorized_user",
1208                "client_id": "test-client-id",
1209                "client_secret": "test-client-secret",
1210                "refresh_token": "test-refresh-token",
1211                "token_uri": server.url("/token").to_string()
1212            }
1213        });
1214        let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1215
1216        let err = token_provider.token().await.unwrap_err();
1217        let original_err = find_source_error::<CredentialsError>(&err).unwrap();
1218        assert!(original_err.is_transient());
1219
1220        Ok(())
1221    }
1222
1223    #[tokio::test]
1224    async fn debug_token_provider() {
1225        let source_credentials = crate::credentials::user_account::Builder::new(json!({
1226            "type": "authorized_user",
1227            "client_id": "test-client-id",
1228            "client_secret": "test-client-secret",
1229            "refresh_token": "test-refresh-token"
1230        }))
1231        .build()
1232        .unwrap();
1233
1234        let expected = ImpersonatedTokenProvider {
1235            source_credentials,
1236            service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1237            delegates: Some(vec!["delegate1".to_string()]),
1238            scopes: vec!["scope1".to_string()],
1239            lifetime: Duration::from_secs(3600),
1240        };
1241        let fmt = format!("{expected:?}");
1242        assert!(fmt.contains("UserCredentials"), "{fmt}");
1243        assert!(fmt.contains("test-client-id"), "{fmt}");
1244        assert!(!fmt.contains("test-client-secret"), "{fmt}");
1245        assert!(!fmt.contains("test-refresh-token"), "{fmt}");
1246        assert!(fmt.contains("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"), "{fmt}");
1247        assert!(fmt.contains("delegate1"), "{fmt}");
1248        assert!(fmt.contains("scope1"), "{fmt}");
1249        assert!(fmt.contains("3600s"), "{fmt}");
1250    }
1251
1252    #[test]
1253    fn impersonated_config_full_from_json_success() {
1254        let source_credentials_json = json!({
1255            "type": "authorized_user",
1256            "client_id": "test-client-id",
1257            "client_secret": "test-client-secret",
1258            "refresh_token": "test-refresh-token"
1259        });
1260        let json = json!({
1261            "type": "impersonated_service_account",
1262            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1263            "source_credentials": source_credentials_json,
1264            "delegates": ["delegate1"],
1265            "quota_project_id": "test-project-id",
1266            "scopes": ["scope1"],
1267        });
1268
1269        let expected = ImpersonatedConfig {
1270            service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1271            source_credentials: source_credentials_json,
1272            delegates: Some(vec!["delegate1".to_string()]),
1273            quota_project_id: Some("test-project-id".to_string()),
1274            scopes: Some(vec!["scope1".to_string()]),
1275        };
1276        let actual: ImpersonatedConfig = serde_json::from_value(json).unwrap();
1277        assert_eq!(actual, expected);
1278    }
1279
1280    #[test]
1281    fn impersonated_config_partial_from_json_success() {
1282        let source_credentials_json = json!({
1283            "type": "authorized_user",
1284            "client_id": "test-client-id",
1285            "client_secret": "test-client-secret",
1286            "refresh_token": "test-refresh-token"
1287        });
1288        let json = json!({
1289            "type": "impersonated_service_account",
1290            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1291            "source_credentials": source_credentials_json
1292        });
1293
1294        let config: ImpersonatedConfig = serde_json::from_value(json).unwrap();
1295        assert_eq!(
1296            config.service_account_impersonation_url,
1297            "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1298        );
1299        assert_eq!(config.source_credentials, source_credentials_json);
1300        assert_eq!(config.delegates, None);
1301        assert_eq!(config.quota_project_id, None);
1302        assert_eq!(config.scopes, None);
1303    }
1304
1305    #[tokio::test]
1306    async fn test_impersonated_service_account_source_fail() -> TestResult {
1307        #[derive(Debug)]
1308        struct MockSourceCredentialsFail;
1309
1310        #[async_trait]
1311        impl CredentialsProvider for MockSourceCredentialsFail {
1312            async fn headers(
1313                &self,
1314                _extensions: Extensions,
1315            ) -> Result<CacheableResource<HeaderMap>> {
1316                Err(errors::non_retryable_from_str("source failed"))
1317            }
1318        }
1319
1320        let source_credentials = Credentials {
1321            inner: Arc::new(MockSourceCredentialsFail),
1322        };
1323
1324        let token_provider = ImpersonatedTokenProvider {
1325            source_credentials,
1326            service_account_impersonation_url: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string(),
1327            delegates: Some(vec!["delegate1".to_string()]),
1328            scopes: vec!["scope1".to_string()],
1329            lifetime: DEFAULT_LIFETIME,
1330        };
1331
1332        let err = token_provider.token().await.unwrap_err();
1333        assert!(err.to_string().contains("source failed"));
1334
1335        Ok(())
1336    }
1337
1338    #[tokio::test]
1339    async fn test_missing_impersonation_url_fail() {
1340        let source_credentials = crate::credentials::user_account::Builder::new(json!({
1341            "type": "authorized_user",
1342            "client_id": "test-client-id",
1343            "client_secret": "test-client-secret",
1344            "refresh_token": "test-refresh-token"
1345        }))
1346        .build()
1347        .unwrap();
1348
1349        let result = Builder::from_source_credentials(source_credentials).build();
1350        assert!(result.is_err());
1351        let err = result.unwrap_err();
1352        assert!(err.is_parsing());
1353        assert!(
1354            err.to_string()
1355                .contains("`service_account_impersonation_url` is required")
1356        );
1357    }
1358
1359    #[tokio::test]
1360    async fn test_nested_impersonated_credentials_fail() {
1361        let nested_impersonated = json!({
1362            "type": "impersonated_service_account",
1363            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1364            "source_credentials": {
1365                "type": "impersonated_service_account",
1366                "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1367                "source_credentials": {
1368                    "type": "authorized_user",
1369                    "client_id": "test-client-id",
1370                    "client_secret": "test-client-secret",
1371                    "refresh_token": "test-refresh-token"
1372                }
1373            }
1374        });
1375
1376        let result = Builder::new(nested_impersonated).build();
1377        assert!(result.is_err());
1378        let err = result.unwrap_err();
1379        assert!(err.is_parsing());
1380        assert!(
1381            err.to_string().contains(
1382                "source credential of type `impersonated_service_account` is not supported"
1383            )
1384        );
1385    }
1386
1387    #[tokio::test]
1388    async fn test_malformed_impersonated_credentials_fail() {
1389        let malformed_impersonated = json!({
1390            "type": "impersonated_service_account",
1391            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1392        });
1393
1394        let result = Builder::new(malformed_impersonated).build();
1395        assert!(result.is_err());
1396        let err = result.unwrap_err();
1397        assert!(err.is_parsing());
1398        assert!(
1399            err.to_string()
1400                .contains("missing field `source_credentials`")
1401        );
1402    }
1403
1404    #[tokio::test]
1405    async fn test_invalid_source_credential_type_fail() {
1406        let invalid_source = json!({
1407            "type": "impersonated_service_account",
1408            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1409            "source_credentials": {
1410                "type": "invalid_type",
1411            }
1412        });
1413
1414        let result = Builder::new(invalid_source).build();
1415        assert!(result.is_err());
1416        let err = result.unwrap_err();
1417        assert!(err.is_unknown_type());
1418    }
1419
1420    #[tokio::test]
1421    async fn test_missing_expiry() -> TestResult {
1422        let server = Server::run();
1423        server.expect(
1424            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1425                json_encoded(json!({
1426                    "access_token": "test-user-account-token",
1427                    "expires_in": 3600,
1428                    "token_type": "Bearer",
1429                })),
1430            ),
1431        );
1432        server.expect(
1433            Expectation::matching(request::method_path(
1434                "POST",
1435                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1436            ))
1437            .respond_with(json_encoded(json!({
1438                "accessToken": "test-impersonated-token",
1439            }))),
1440        );
1441
1442        let impersonated_credential = json!({
1443            "type": "impersonated_service_account",
1444            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1445            "source_credentials": {
1446                "type": "authorized_user",
1447                "client_id": "test-client-id",
1448                "client_secret": "test-client-secret",
1449                "refresh_token": "test-refresh-token",
1450                "token_uri": server.url("/token").to_string()
1451            }
1452        });
1453        let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1454
1455        let err = token_provider.token().await.unwrap_err();
1456        assert!(!err.is_transient());
1457
1458        Ok(())
1459    }
1460
1461    #[tokio::test]
1462    async fn test_invalid_expiry_format() -> TestResult {
1463        let server = Server::run();
1464        server.expect(
1465            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1466                json_encoded(json!({
1467                    "access_token": "test-user-account-token",
1468                    "expires_in": 3600,
1469                    "token_type": "Bearer",
1470                })),
1471            ),
1472        );
1473        server.expect(
1474            Expectation::matching(request::method_path(
1475                "POST",
1476                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1477            ))
1478            .respond_with(json_encoded(json!({
1479                "accessToken": "test-impersonated-token",
1480                "expireTime": "invalid-format"
1481            }))),
1482        );
1483
1484        let impersonated_credential = json!({
1485            "type": "impersonated_service_account",
1486            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1487            "source_credentials": {
1488                "type": "authorized_user",
1489                "client_id": "test-client-id",
1490                "client_secret": "test-client-secret",
1491                "refresh_token": "test-refresh-token",
1492                "token_uri": server.url("/token").to_string()
1493            }
1494        });
1495        let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1496
1497        let err = token_provider.token().await.unwrap_err();
1498        assert!(!err.is_transient());
1499
1500        Ok(())
1501    }
1502
1503    #[tokio::test]
1504    async fn token_provider_malformed_response_is_nonretryable() -> TestResult {
1505        let server = Server::run();
1506        server.expect(
1507            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1508                json_encoded(json!({
1509                    "access_token": "test-user-account-token",
1510                    "expires_in": 3600,
1511                    "token_type": "Bearer",
1512                })),
1513            ),
1514        );
1515        server.expect(
1516            Expectation::matching(request::method_path(
1517                "POST",
1518                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1519            ))
1520            .respond_with(json_encoded(json!("bad json"))),
1521        );
1522
1523        let impersonated_credential = json!({
1524            "type": "impersonated_service_account",
1525            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1526            "source_credentials": {
1527                "type": "authorized_user",
1528                "client_id": "test-client-id",
1529                "client_secret": "test-client-secret",
1530                "refresh_token": "test-refresh-token",
1531                "token_uri": server.url("/token").to_string()
1532            }
1533        });
1534        let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1535
1536        let e = token_provider.token().await.err().unwrap();
1537        assert!(!e.is_transient(), "{e}");
1538
1539        Ok(())
1540    }
1541
1542    #[tokio::test]
1543    async fn token_provider_nonretryable_error() -> TestResult {
1544        let server = Server::run();
1545        server.expect(
1546            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1547                json_encoded(json!({
1548                    "access_token": "test-user-account-token",
1549                    "expires_in": 3600,
1550                    "token_type": "Bearer",
1551                })),
1552            ),
1553        );
1554        server.expect(
1555            Expectation::matching(request::method_path(
1556                "POST",
1557                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1558            ))
1559            .respond_with(status_code(401)),
1560        );
1561
1562        let impersonated_credential = json!({
1563            "type": "impersonated_service_account",
1564            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1565            "source_credentials": {
1566                "type": "authorized_user",
1567                "client_id": "test-client-id",
1568                "client_secret": "test-client-secret",
1569                "refresh_token": "test-refresh-token",
1570                "token_uri": server.url("/token").to_string()
1571            }
1572        });
1573        let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1574
1575        let err = token_provider.token().await.unwrap_err();
1576        assert!(!err.is_transient());
1577
1578        Ok(())
1579    }
1580
1581    #[tokio::test]
1582    async fn credential_full_with_quota_project_from_builder() -> TestResult {
1583        let server = Server::run();
1584        server.expect(
1585            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1586                json_encoded(json!({
1587                    "access_token": "test-user-account-token",
1588                    "expires_in": 3600,
1589                    "token_type": "Bearer",
1590                })),
1591            ),
1592        );
1593        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1594            .format(&time::format_description::well_known::Rfc3339)
1595            .unwrap();
1596        server.expect(
1597            Expectation::matching(request::method_path(
1598                "POST",
1599                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1600            ))
1601            .respond_with(json_encoded(json!({
1602                "accessToken": "test-impersonated-token",
1603                "expireTime": expire_time
1604            }))),
1605        );
1606
1607        let impersonated_credential = json!({
1608            "type": "impersonated_service_account",
1609            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1610            "source_credentials": {
1611                "type": "authorized_user",
1612                "client_id": "test-client-id",
1613                "client_secret": "test-client-secret",
1614                "refresh_token": "test-refresh-token",
1615                "token_uri": server.url("/token").to_string()
1616            }
1617        });
1618        let creds = Builder::new(impersonated_credential)
1619            .with_quota_project_id("test-project")
1620            .build()?;
1621
1622        let headers = creds.headers(Extensions::new()).await?;
1623        match headers {
1624            CacheableResource::New { data, .. } => {
1625                assert_eq!(data.get("x-goog-user-project").unwrap(), "test-project");
1626            }
1627            CacheableResource::NotModified => panic!("Expected new headers, but got NotModified"),
1628        }
1629
1630        Ok(())
1631    }
1632
1633    #[tokio::test]
1634    async fn access_token_credentials_success() -> TestResult {
1635        let server = Server::run();
1636        server.expect(
1637            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1638                json_encoded(json!({
1639                    "access_token": "test-user-account-token",
1640                    "expires_in": 3600,
1641                    "token_type": "Bearer",
1642                })),
1643            ),
1644        );
1645        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1646            .format(&time::format_description::well_known::Rfc3339)
1647            .unwrap();
1648        server.expect(
1649            Expectation::matching(request::method_path(
1650                "POST",
1651                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1652            ))
1653            .respond_with(json_encoded(json!({
1654                "accessToken": "test-impersonated-token",
1655                "expireTime": expire_time
1656            }))),
1657        );
1658
1659        let impersonated_credential = json!({
1660            "type": "impersonated_service_account",
1661            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1662            "source_credentials": {
1663                "type": "authorized_user",
1664                "client_id": "test-client-id",
1665                "client_secret": "test-client-secret",
1666                "refresh_token": "test-refresh-token",
1667                "token_uri": server.url("/token").to_string()
1668            }
1669        });
1670        let creds = Builder::new(impersonated_credential).build_access_token_credentials()?;
1671
1672        let access_token = creds.access_token().await?;
1673        assert_eq!(access_token.token, "test-impersonated-token");
1674
1675        Ok(())
1676    }
1677
1678    #[tokio::test]
1679    async fn test_with_target_principal() {
1680        let source_credentials = crate::credentials::user_account::Builder::new(json!({
1681            "type": "authorized_user",
1682            "client_id": "test-client-id",
1683            "client_secret": "test-client-secret",
1684            "refresh_token": "test-refresh-token"
1685        }))
1686        .build()
1687        .unwrap();
1688
1689        let (token_provider, _) = Builder::from_source_credentials(source_credentials)
1690            .with_target_principal("test-principal@example.iam.gserviceaccount.com")
1691            .build_components()
1692            .unwrap();
1693
1694        assert_eq!(
1695            token_provider.inner.service_account_impersonation_url,
1696            "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal@example.iam.gserviceaccount.com:generateAccessToken"
1697        );
1698    }
1699
1700    #[tokio::test]
1701    async fn credential_full_with_quota_project_from_json() -> TestResult {
1702        let server = Server::run();
1703        server.expect(
1704            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1705                json_encoded(json!({
1706                    "access_token": "test-user-account-token",
1707                    "expires_in": 3600,
1708                    "token_type": "Bearer",
1709                })),
1710            ),
1711        );
1712        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1713            .format(&time::format_description::well_known::Rfc3339)
1714            .unwrap();
1715        server.expect(
1716            Expectation::matching(request::method_path(
1717                "POST",
1718                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
1719            ))
1720            .respond_with(json_encoded(json!({
1721                "accessToken": "test-impersonated-token",
1722                "expireTime": expire_time
1723            }))),
1724        );
1725
1726        let impersonated_credential = json!({
1727            "type": "impersonated_service_account",
1728            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1729            "source_credentials": {
1730                "type": "authorized_user",
1731                "client_id": "test-client-id",
1732                "client_secret": "test-client-secret",
1733                "refresh_token": "test-refresh-token",
1734                "token_uri": server.url("/token").to_string()
1735            },
1736            "quota_project_id": "test-project-from-json",
1737        });
1738
1739        let creds = Builder::new(impersonated_credential).build()?;
1740
1741        let headers = creds.headers(Extensions::new()).await?;
1742        match headers {
1743            CacheableResource::New { data, .. } => {
1744                assert_eq!(
1745                    data.get("x-goog-user-project").unwrap(),
1746                    "test-project-from-json"
1747                );
1748            }
1749            CacheableResource::NotModified => panic!("Expected new headers, but got NotModified"),
1750        }
1751
1752        Ok(())
1753    }
1754
1755    #[tokio::test]
1756    async fn test_impersonated_does_not_propagate_settings_to_source() -> TestResult {
1757        let server = Server::run();
1758
1759        // Expectation for the source credential token request.
1760        // It should NOT have any scopes in the body.
1761        server.expect(
1762            Expectation::matching(all_of![
1763                request::method_path("POST", "/source_token"),
1764                request::body(json_decoded(
1765                    |body: &serde_json::Value| body["scopes"].is_null()
1766                ))
1767            ])
1768            .respond_with(json_encoded(json!({
1769                "access_token": "source-token",
1770                "expires_in": 3600,
1771                "token_type": "Bearer",
1772            }))),
1773        );
1774
1775        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1776            .format(&time::format_description::well_known::Rfc3339)
1777            .unwrap();
1778
1779        // Expectation for the impersonation request.
1780        // It SHOULD have the scopes from the impersonated builder.
1781        server.expect(
1782            Expectation::matching(all_of![
1783                request::method_path(
1784                    "POST",
1785                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1786                ),
1787                request::headers(contains(("authorization", "Bearer source-token"))),
1788                request::body(json_decoded(eq(json!({
1789                    "scope": ["impersonated-scope"],
1790                    "lifetime": "3600s"
1791                }))))
1792            ])
1793            .respond_with(json_encoded(json!({
1794                "accessToken": "impersonated-token",
1795                "expireTime": expire_time
1796            }))),
1797        );
1798
1799        let impersonated_credential = json!({
1800            "type": "impersonated_service_account",
1801            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1802            "source_credentials": {
1803                "type": "authorized_user",
1804                "client_id": "test-client-id",
1805                "client_secret": "test-client-secret",
1806                "refresh_token": "test-refresh-token",
1807                "token_uri": server.url("/source_token").to_string()
1808            }
1809        });
1810
1811        let creds = Builder::new(impersonated_credential)
1812            .with_scopes(vec!["impersonated-scope"])
1813            .with_quota_project_id("impersonated-quota-project")
1814            .build()?;
1815
1816        // The quota project should be set on the final credentials object.
1817        let fmt = format!("{creds:?}");
1818        assert!(fmt.contains("impersonated-quota-project"));
1819
1820        // Fetching the token will trigger the mock server expectations.
1821        let _token = creds.headers(Extensions::new()).await?;
1822
1823        Ok(())
1824    }
1825
1826    #[tokio::test]
1827    async fn test_impersonated_metrics_header() -> TestResult {
1828        let server = Server::run();
1829        server.expect(
1830            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1831                json_encoded(json!({
1832                    "access_token": "test-user-account-token",
1833                    "expires_in": 3600,
1834                    "token_type": "Bearer",
1835                })),
1836            ),
1837        );
1838        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1839            .format(&time::format_description::well_known::Rfc3339)
1840            .unwrap();
1841        server.expect(
1842            Expectation::matching(all_of![
1843                request::method_path(
1844                    "POST",
1845                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1846                ),
1847                request::headers(contains(("x-goog-api-client", matches("cred-type/imp")))),
1848                request::headers(contains((
1849                    "x-goog-api-client",
1850                    matches("auth-request-type/at")
1851                )))
1852            ])
1853            .respond_with(json_encoded(json!({
1854                "accessToken": "test-impersonated-token",
1855                "expireTime": expire_time
1856            }))),
1857        );
1858
1859        let impersonated_credential = json!({
1860            "type": "impersonated_service_account",
1861            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1862            "source_credentials": {
1863                "type": "authorized_user",
1864                "client_id": "test-client-id",
1865                "client_secret": "test-client-secret",
1866                "refresh_token": "test-refresh-token",
1867                "token_uri": server.url("/token").to_string()
1868            }
1869        });
1870        let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1871
1872        let token = token_provider.token().await?;
1873        assert_eq!(token.token, "test-impersonated-token");
1874        assert_eq!(token.token_type, "Bearer");
1875
1876        Ok(())
1877    }
1878
1879    #[tokio::test]
1880    async fn test_impersonated_retries_for_success() -> TestResult {
1881        let mut server = Server::run();
1882        // Source credential token endpoint
1883        server.expect(
1884            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1885                json_encoded(json!({
1886                    "access_token": "test-user-account-token",
1887                    "expires_in": 3600,
1888                    "token_type": "Bearer",
1889                })),
1890            ),
1891        );
1892
1893        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1894            .format(&time::format_description::well_known::Rfc3339)
1895            .unwrap();
1896
1897        // Impersonation endpoint
1898        let impersonation_path =
1899            "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
1900        server.expect(
1901            Expectation::matching(request::method_path("POST", impersonation_path))
1902                .times(3)
1903                .respond_with(cycle![
1904                    status_code(503).body("try-again"),
1905                    status_code(503).body("try-again"),
1906                    status_code(200)
1907                        .append_header("Content-Type", "application/json")
1908                        .body(
1909                            json!({
1910                                "accessToken": "test-impersonated-token",
1911                                "expireTime": expire_time
1912                            })
1913                            .to_string()
1914                        ),
1915                ]),
1916        );
1917
1918        let impersonated_credential = json!({
1919            "type": "impersonated_service_account",
1920            "service_account_impersonation_url": server.url(impersonation_path).to_string(),
1921            "source_credentials": {
1922                "type": "authorized_user",
1923                "client_id": "test-client-id",
1924                "client_secret": "test-client-secret",
1925                "refresh_token": "test-refresh-token",
1926                "token_uri": server.url("/token").to_string()
1927            }
1928        });
1929
1930        let (token_provider, _) = Builder::new(impersonated_credential)
1931            .with_retry_policy(get_mock_auth_retry_policy(3))
1932            .with_backoff_policy(get_mock_backoff_policy())
1933            .with_retry_throttler(get_mock_retry_throttler())
1934            .build_components()?;
1935
1936        let token = token_provider.token().await?;
1937        assert_eq!(token.token, "test-impersonated-token");
1938
1939        server.verify_and_clear();
1940        Ok(())
1941    }
1942
1943    #[tokio::test]
1944    async fn test_scopes_from_json() -> TestResult {
1945        let server = Server::run();
1946        server.expect(
1947            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1948                json_encoded(json!({
1949                    "access_token": "test-user-account-token",
1950                    "expires_in": 3600,
1951                    "token_type": "Bearer",
1952                })),
1953            ),
1954        );
1955        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1956            .format(&time::format_description::well_known::Rfc3339)
1957            .unwrap();
1958        server.expect(
1959            Expectation::matching(all_of![
1960                request::method_path(
1961                    "POST",
1962                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
1963                ),
1964                request::body(json_decoded(eq(json!({
1965                    "scope": ["scope-from-json"],
1966                    "lifetime": "3600s"
1967                }))))
1968            ])
1969            .respond_with(json_encoded(json!({
1970                "accessToken": "test-impersonated-token",
1971                "expireTime": expire_time
1972            }))),
1973        );
1974
1975        let impersonated_credential = json!({
1976            "type": "impersonated_service_account",
1977            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
1978            "scopes": ["scope-from-json"],
1979            "source_credentials": {
1980                "type": "authorized_user",
1981                "client_id": "test-client-id",
1982                "client_secret": "test-client-secret",
1983                "refresh_token": "test-refresh-token",
1984                "token_uri": server.url("/token").to_string()
1985            }
1986        });
1987        let (token_provider, _) = Builder::new(impersonated_credential).build_components()?;
1988
1989        let token = token_provider.token().await?;
1990        assert_eq!(token.token, "test-impersonated-token");
1991
1992        Ok(())
1993    }
1994
1995    #[tokio::test]
1996    async fn test_with_scopes_overrides_json_scopes() -> TestResult {
1997        let server = Server::run();
1998        server.expect(
1999            Expectation::matching(request::method_path("POST", "/token")).respond_with(
2000                json_encoded(json!({
2001                    "access_token": "test-user-account-token",
2002                    "expires_in": 3600,
2003                    "token_type": "Bearer",
2004                })),
2005            ),
2006        );
2007        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2008            .format(&time::format_description::well_known::Rfc3339)
2009            .unwrap();
2010        server.expect(
2011            Expectation::matching(all_of![
2012                request::method_path(
2013                    "POST",
2014                    "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken"
2015                ),
2016                request::body(json_decoded(eq(json!({
2017                    "scope": ["scope-from-with-scopes"],
2018                    "lifetime": "3600s"
2019                }))))
2020            ])
2021            .respond_with(json_encoded(json!({
2022                "accessToken": "test-impersonated-token",
2023                "expireTime": expire_time
2024            }))),
2025        );
2026
2027        let impersonated_credential = json!({
2028            "type": "impersonated_service_account",
2029            "service_account_impersonation_url": server.url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken").to_string(),
2030            "scopes": ["scope-from-json"],
2031            "source_credentials": {
2032                "type": "authorized_user",
2033                "client_id": "test-client-id",
2034                "client_secret": "test-client-secret",
2035                "refresh_token": "test-refresh-token",
2036                "token_uri": server.url("/token").to_string()
2037            }
2038        });
2039        let (token_provider, _) = Builder::new(impersonated_credential)
2040            .with_scopes(vec!["scope-from-with-scopes"])
2041            .build_components()?;
2042
2043        let token = token_provider.token().await?;
2044        assert_eq!(token.token, "test-impersonated-token");
2045
2046        Ok(())
2047    }
2048
2049    #[tokio::test]
2050    async fn test_impersonated_does_not_retry_on_non_transient_failures() -> TestResult {
2051        let mut server = Server::run();
2052        // Source credential token endpoint
2053        server.expect(
2054            Expectation::matching(request::method_path("POST", "/token")).respond_with(
2055                json_encoded(json!({
2056                    "access_token": "test-user-account-token",
2057                    "expires_in": 3600,
2058                    "token_type": "Bearer",
2059                })),
2060            ),
2061        );
2062
2063        // Impersonation endpoint
2064        let impersonation_path =
2065            "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken";
2066        server.expect(
2067            Expectation::matching(request::method_path("POST", impersonation_path))
2068                .times(1)
2069                .respond_with(status_code(401)),
2070        );
2071
2072        let impersonated_credential = json!({
2073            "type": "impersonated_service_account",
2074            "service_account_impersonation_url": server.url(impersonation_path).to_string(),
2075            "source_credentials": {
2076                "type": "authorized_user",
2077                "client_id": "test-client-id",
2078                "client_secret": "test-client-secret",
2079                "refresh_token": "test-refresh-token",
2080                "token_uri": server.url("/token").to_string()
2081            }
2082        });
2083
2084        let (token_provider, _) = Builder::new(impersonated_credential)
2085            .with_retry_policy(get_mock_auth_retry_policy(3))
2086            .with_backoff_policy(get_mock_backoff_policy())
2087            .with_retry_throttler(get_mock_retry_throttler())
2088            .build_components()?;
2089
2090        let err = token_provider.token().await.unwrap_err();
2091        assert!(!err.is_transient());
2092
2093        server.verify_and_clear();
2094        Ok(())
2095    }
2096
2097    #[tokio::test]
2098    async fn test_impersonated_remote_signer() -> TestResult {
2099        use base64::{Engine, prelude::BASE64_STANDARD};
2100
2101        let server = Server::run();
2102        server.expect(
2103            Expectation::matching(request::method_path("POST", "/token"))
2104                .times(2..)
2105                .respond_with(json_encoded(json!({
2106                    "access_token": "test-user-account-token",
2107                    "expires_in": 3600,
2108                    "token_type": "Bearer",
2109                }))),
2110        );
2111        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
2112            .format(&time::format_description::well_known::Rfc3339)
2113            .unwrap();
2114        server.expect(
2115            Expectation::matching(request::method_path(
2116                "POST",
2117                "/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2118            ))
2119            .times(2)
2120            .respond_with(json_encoded(json!({
2121                "accessToken": "test-impersonated-token",
2122                "expireTime": expire_time
2123            }))),
2124        );
2125
2126        server.expect(
2127            Expectation::matching(all_of![
2128                request::method_path(
2129                    "POST",
2130                    "/v1/projects/-/serviceAccounts/test-principal:signBlob"
2131                ),
2132                request::headers(contains((
2133                    "authorization",
2134                    "Bearer test-impersonated-token"
2135                ))),
2136            ])
2137            .times(2)
2138            .respond_with(json_encoded(json!({
2139                "signedBlob": BASE64_STANDARD.encode("signed_blob"),
2140            }))),
2141        );
2142        let impersonation_url = server
2143            .url("/v1/projects/-/serviceAccounts/test-principal:generateAccessToken")
2144            .to_string();
2145
2146        // Test builder from source credentials
2147        let user_credential = json!({
2148            "type": "authorized_user",
2149            "client_id": "test-client-id",
2150            "client_secret": "test-client-secret",
2151            "refresh_token": "test-refresh-token",
2152            "token_uri": server.url("/token").to_string()
2153        });
2154        let source_credential =
2155            crate::credentials::user_account::Builder::new(user_credential.clone()).build()?;
2156        let mut builder_from_source = Builder::from_source_credentials(source_credential)
2157            .with_target_principal("test-principal");
2158        builder_from_source.service_account_impersonation_url = Some(impersonation_url.clone());
2159
2160        // Test builder from JSON
2161        let impersonated_credential = json!({
2162            "type": "impersonated_service_account",
2163            "service_account_impersonation_url": impersonation_url,
2164            "source_credentials": user_credential,
2165        });
2166        let builder_from_json = Builder::new(impersonated_credential);
2167
2168        for builder in [builder_from_source, builder_from_json] {
2169            let iam_endpoint = server.url("").to_string().trim_end_matches('/').to_string();
2170            let signer = builder.build_signer_with_iam_endpoint_override(Some(iam_endpoint))?;
2171
2172            let client_email = signer.client_email().await?;
2173            assert_eq!(client_email, "test-principal");
2174
2175            let result = signer.sign(b"test").await?;
2176            assert_eq!(result.as_ref(), b"signed_blob");
2177        }
2178
2179        Ok(())
2180    }
2181
2182    #[tokio::test]
2183    async fn test_impersonated_sa_signer() -> TestResult {
2184        use crate::credentials::service_account::ServiceAccountKey;
2185        use crate::credentials::tests::PKCS8_PK;
2186        use serde_json::Value;
2187
2188        let service_account = json!({
2189            "type": "service_account",
2190            "client_email": "test-client-email",
2191            "private_key_id": "test-private-key-id",
2192            "private_key": Value::from(PKCS8_PK.clone()),
2193            "project_id": "test-project-id",
2194        });
2195        let impersonated_credential = json!({
2196            "type": "impersonated_service_account",
2197            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken",
2198            "source_credentials": service_account.clone(),
2199        });
2200
2201        let signer = Builder::new(impersonated_credential).build_signer()?;
2202
2203        let client_email = signer.client_email().await?;
2204        assert_eq!(client_email, "test-principal");
2205
2206        let result = signer.sign(b"test").await?;
2207
2208        let service_account_key = serde_json::from_value::<ServiceAccountKey>(service_account)?;
2209        let inner_signer = service_account_key.signer().unwrap();
2210        let inner_result = inner_signer.sign(b"test")?;
2211        assert_eq!(result.as_ref(), inner_result);
2212
2213        Ok(())
2214    }
2215
2216    #[tokio::test]
2217    async fn test_impersonated_signer_with_invalid_email() -> TestResult {
2218        let impersonated_credential = json!({
2219            "type": "impersonated_service_account",
2220            "service_account_impersonation_url": "http://example.com/test-principal:generateIdToken",
2221            "source_credentials": json!({
2222                "type": "service_account",
2223                "client_email": "test-client-email",
2224                "private_key_id": "test-private-key-id",
2225                "private_key": "test-private-key",
2226                "project_id": "test-project-id",
2227            }),
2228        });
2229
2230        let error = Builder::new(impersonated_credential)
2231            .build_signer()
2232            .unwrap_err();
2233
2234        assert!(error.is_parsing());
2235        assert!(
2236            error
2237                .to_string()
2238                .contains("invalid service account impersonation URL"),
2239            "error: {}",
2240            error
2241        );
2242
2243        Ok(())
2244    }
2245}