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