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