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