Skip to main content

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 google_cloud_gax::retry_policy::{AlwaysRetry, RetryPolicyExt};
73//! use google_cloud_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::AuthHeadersBuilder;
125use crate::retry::Builder as RetryTokenProviderBuilder;
126use crate::token::{CachedTokenProvider, Token, TokenProvider};
127use crate::token_cache::TokenCache;
128use crate::{BuildResult, Result};
129use google_cloud_gax::backoff_policy::BackoffPolicyArg;
130use google_cloud_gax::retry_policy::RetryPolicyArg;
131use google_cloud_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 google_cloud_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 google_cloud_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 google_cloud_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 google_cloud_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 google_cloud_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 google_cloud_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
1283        AuthHeadersBuilder::new(&token)
1284            .maybe_quota_project_id(self.quota_project_id.as_deref())
1285            .build()
1286    }
1287}
1288
1289#[async_trait::async_trait]
1290impl<T> AccessTokenCredentialsProvider for ExternalAccountCredentials<T>
1291where
1292    T: CachedTokenProvider,
1293{
1294    async fn access_token(&self) -> Result<AccessToken> {
1295        let token = self.token_provider.token(Extensions::new()).await?;
1296        token.into()
1297    }
1298}
1299
1300#[cfg(test)]
1301mod tests {
1302    use super::*;
1303    use crate::constants::{
1304        ACCESS_TOKEN_TYPE, DEFAULT_SCOPE, JWT_TOKEN_TYPE, TOKEN_EXCHANGE_GRANT_TYPE,
1305    };
1306    use crate::credentials::subject_token::{
1307        Builder as SubjectTokenBuilder, SubjectToken, SubjectTokenProvider,
1308    };
1309    use crate::credentials::tests::{
1310        find_source_error, get_mock_auth_retry_policy, get_mock_backoff_policy,
1311        get_mock_retry_throttler,
1312    };
1313    use crate::errors::{CredentialsError, SubjectTokenProviderError};
1314    use httptest::{
1315        Expectation, Server, cycle,
1316        matchers::{all_of, contains, request, url_decoded},
1317        responders::{json_encoded, status_code},
1318    };
1319    use serde_json::*;
1320    use std::collections::HashMap;
1321    use std::error::Error;
1322    use std::fmt;
1323    use test_case::test_case;
1324    use time::OffsetDateTime;
1325
1326    #[derive(Debug)]
1327    struct TestProviderError;
1328    impl fmt::Display for TestProviderError {
1329        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1330            write!(f, "TestProviderError")
1331        }
1332    }
1333    impl Error for TestProviderError {}
1334    impl SubjectTokenProviderError for TestProviderError {
1335        fn is_transient(&self) -> bool {
1336            false
1337        }
1338    }
1339
1340    #[derive(Debug)]
1341    struct TestSubjectTokenProvider;
1342    impl SubjectTokenProvider for TestSubjectTokenProvider {
1343        type Error = TestProviderError;
1344        async fn subject_token(&self) -> std::result::Result<SubjectToken, Self::Error> {
1345            Ok(SubjectTokenBuilder::new("test-subject-token".to_string()).build())
1346        }
1347    }
1348
1349    #[tokio::test]
1350    async fn create_external_account_builder() {
1351        let contents = json!({
1352            "type": "external_account",
1353            "audience": "audience",
1354            "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1355            "token_url": "https://sts.googleapis.com/v1beta/token",
1356            "credential_source": {
1357                "url": "https://example.com/token",
1358                "format": {
1359                  "type": "json",
1360                  "subject_token_field_name": "access_token"
1361                }
1362            }
1363        });
1364
1365        let creds = Builder::new(contents)
1366            .with_quota_project_id("test_project")
1367            .with_scopes(["a", "b"])
1368            .build()
1369            .unwrap();
1370
1371        // Use the debug output to verify the right kind of credentials are created.
1372        let fmt = format!("{creds:?}");
1373        assert!(fmt.contains("ExternalAccountCredentials"));
1374    }
1375
1376    #[tokio::test]
1377    async fn create_external_account_detect_url_sourced() {
1378        let contents = json!({
1379            "type": "external_account",
1380            "audience": "audience",
1381            "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1382            "token_url": "https://sts.googleapis.com/v1beta/token",
1383            "credential_source": {
1384                "url": "https://example.com/token",
1385                "headers": {
1386                  "Metadata": "True"
1387                },
1388                "format": {
1389                  "type": "json",
1390                  "subject_token_field_name": "access_token"
1391                }
1392            }
1393        });
1394
1395        let file: ExternalAccountFile =
1396            serde_json::from_value(contents).expect("failed to parse external account config");
1397        let config: ExternalAccountConfig = file.into();
1398        let source = config.credential_source;
1399
1400        match source {
1401            CredentialSource::Url(source) => {
1402                assert_eq!(source.url, "https://example.com/token");
1403                assert_eq!(
1404                    source.headers,
1405                    HashMap::from([("Metadata".to_string(), "True".to_string()),]),
1406                );
1407                assert_eq!(source.format, "json");
1408                assert_eq!(source.subject_token_field_name, "access_token");
1409            }
1410            _ => {
1411                unreachable!("expected Url Sourced credential")
1412            }
1413        }
1414    }
1415
1416    #[tokio::test]
1417    async fn create_external_account_detect_executable_sourced() {
1418        let contents = json!({
1419            "type": "external_account",
1420            "audience": "audience",
1421            "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1422            "token_url": "https://sts.googleapis.com/v1beta/token",
1423            "credential_source": {
1424                "executable": {
1425                    "command": "cat /some/file",
1426                    "output_file": "/some/file",
1427                    "timeout_millis": 5000
1428                }
1429            }
1430        });
1431
1432        let file: ExternalAccountFile =
1433            serde_json::from_value(contents).expect("failed to parse external account config");
1434        let config: ExternalAccountConfig = file.into();
1435        let source = config.credential_source;
1436
1437        match source {
1438            CredentialSource::Executable(source) => {
1439                assert_eq!(source.command, "cat");
1440                assert_eq!(source.args, vec!["/some/file"]);
1441                assert_eq!(source.output_file.as_deref(), Some("/some/file"));
1442                assert_eq!(source.timeout, Duration::from_secs(5));
1443            }
1444            _ => {
1445                unreachable!("expected Executable Sourced credential")
1446            }
1447        }
1448    }
1449
1450    #[tokio::test]
1451    async fn create_external_account_detect_file_sourced() {
1452        let contents = json!({
1453            "type": "external_account",
1454            "audience": "audience",
1455            "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1456            "token_url": "https://sts.googleapis.com/v1beta/token",
1457            "credential_source": {
1458                "file": "/foo/bar",
1459                "format": {
1460                    "type": "json",
1461                    "subject_token_field_name": "token"
1462                }
1463            }
1464        });
1465
1466        let file: ExternalAccountFile =
1467            serde_json::from_value(contents).expect("failed to parse external account config");
1468        let config: ExternalAccountConfig = file.into();
1469        let source = config.credential_source;
1470
1471        match source {
1472            CredentialSource::File(source) => {
1473                assert_eq!(source.file, "/foo/bar");
1474                assert_eq!(source.format, "json");
1475                assert_eq!(source.subject_token_field_name, "token");
1476            }
1477            _ => {
1478                unreachable!("expected File Sourced credential")
1479            }
1480        }
1481    }
1482
1483    #[tokio::test]
1484    async fn test_external_account_with_impersonation_success() {
1485        let subject_token_server = Server::run();
1486        let sts_server = Server::run();
1487        let impersonation_server = Server::run();
1488
1489        let impersonation_path = "/projects/-/serviceAccounts/sa@test.com:generateAccessToken";
1490        let contents = json!({
1491            "type": "external_account",
1492            "audience": "audience",
1493            "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1494            "token_url": sts_server.url("/token").to_string(),
1495            "service_account_impersonation_url": impersonation_server.url(impersonation_path).to_string(),
1496            "credential_source": {
1497                "url": subject_token_server.url("/subject_token").to_string(),
1498                "format": {
1499                  "type": "json",
1500                  "subject_token_field_name": "access_token"
1501                }
1502            }
1503        });
1504
1505        subject_token_server.expect(
1506            Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1507                json_encoded(json!({
1508                    "access_token": "subject_token",
1509                })),
1510            ),
1511        );
1512
1513        sts_server.expect(
1514            Expectation::matching(all_of![
1515                request::method_path("POST", "/token"),
1516                request::body(url_decoded(contains((
1517                    "grant_type",
1518                    TOKEN_EXCHANGE_GRANT_TYPE
1519                )))),
1520                request::body(url_decoded(contains(("subject_token", "subject_token")))),
1521                request::body(url_decoded(contains((
1522                    "requested_token_type",
1523                    ACCESS_TOKEN_TYPE
1524                )))),
1525                request::body(url_decoded(contains((
1526                    "subject_token_type",
1527                    JWT_TOKEN_TYPE
1528                )))),
1529                request::body(url_decoded(contains(("audience", "audience")))),
1530                request::body(url_decoded(contains(("scope", IAM_SCOPE)))),
1531            ])
1532            .respond_with(json_encoded(json!({
1533                "access_token": "sts-token",
1534                "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1535                "token_type": "Bearer",
1536                "expires_in": 3600,
1537            }))),
1538        );
1539
1540        let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1541            .format(&time::format_description::well_known::Rfc3339)
1542            .unwrap();
1543        impersonation_server.expect(
1544            Expectation::matching(all_of![
1545                request::method_path("POST", impersonation_path),
1546                request::headers(contains(("authorization", "Bearer sts-token"))),
1547            ])
1548            .respond_with(json_encoded(json!({
1549                "accessToken": "final-impersonated-token",
1550                "expireTime": expire_time
1551            }))),
1552        );
1553
1554        let creds = Builder::new(contents).build().unwrap();
1555        let headers = creds.headers(Extensions::new()).await.unwrap();
1556        match headers {
1557            CacheableResource::New { data, .. } => {
1558                let token = data.get("authorization").unwrap().to_str().unwrap();
1559                assert_eq!(token, "Bearer final-impersonated-token");
1560            }
1561            CacheableResource::NotModified => panic!("Expected new headers"),
1562        }
1563    }
1564
1565    #[tokio::test]
1566    async fn test_external_account_without_impersonation_success() {
1567        let subject_token_server = Server::run();
1568        let sts_server = Server::run();
1569
1570        let contents = json!({
1571            "type": "external_account",
1572            "audience": "audience",
1573            "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1574            "token_url": sts_server.url("/token").to_string(),
1575            "credential_source": {
1576                "url": subject_token_server.url("/subject_token").to_string(),
1577                "format": {
1578                  "type": "json",
1579                  "subject_token_field_name": "access_token"
1580                }
1581            }
1582        });
1583
1584        subject_token_server.expect(
1585            Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1586                json_encoded(json!({
1587                    "access_token": "subject_token",
1588                })),
1589            ),
1590        );
1591
1592        sts_server.expect(
1593            Expectation::matching(all_of![
1594                request::method_path("POST", "/token"),
1595                request::body(url_decoded(contains((
1596                    "grant_type",
1597                    TOKEN_EXCHANGE_GRANT_TYPE
1598                )))),
1599                request::body(url_decoded(contains(("subject_token", "subject_token")))),
1600                request::body(url_decoded(contains((
1601                    "requested_token_type",
1602                    ACCESS_TOKEN_TYPE
1603                )))),
1604                request::body(url_decoded(contains((
1605                    "subject_token_type",
1606                    JWT_TOKEN_TYPE
1607                )))),
1608                request::body(url_decoded(contains(("audience", "audience")))),
1609                request::body(url_decoded(contains(("scope", DEFAULT_SCOPE)))),
1610            ])
1611            .respond_with(json_encoded(json!({
1612                "access_token": "sts-only-token",
1613                "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1614                "token_type": "Bearer",
1615                "expires_in": 3600,
1616            }))),
1617        );
1618
1619        let creds = Builder::new(contents).build().unwrap();
1620        let headers = creds.headers(Extensions::new()).await.unwrap();
1621        match headers {
1622            CacheableResource::New { data, .. } => {
1623                let token = data.get("authorization").unwrap().to_str().unwrap();
1624                assert_eq!(token, "Bearer sts-only-token");
1625            }
1626            CacheableResource::NotModified => panic!("Expected new headers"),
1627        }
1628    }
1629
1630    #[tokio::test]
1631    async fn test_external_account_access_token_credentials_success() {
1632        let server = Server::run();
1633
1634        let contents = json!({
1635            "type": "external_account",
1636            "audience": "audience",
1637            "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1638            "token_url": server.url("/token").to_string(),
1639            "credential_source": {
1640                "url": server.url("/subject_token").to_string(),
1641                "format": {
1642                  "type": "json",
1643                  "subject_token_field_name": "access_token"
1644                }
1645            }
1646        });
1647
1648        server.expect(
1649            Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1650                json_encoded(json!({
1651                    "access_token": "subject_token",
1652                })),
1653            ),
1654        );
1655
1656        server.expect(
1657            Expectation::matching(all_of![
1658                request::method_path("POST", "/token"),
1659                request::body(url_decoded(contains((
1660                    "grant_type",
1661                    TOKEN_EXCHANGE_GRANT_TYPE
1662                )))),
1663                request::body(url_decoded(contains(("subject_token", "subject_token")))),
1664                request::body(url_decoded(contains((
1665                    "requested_token_type",
1666                    ACCESS_TOKEN_TYPE
1667                )))),
1668                request::body(url_decoded(contains((
1669                    "subject_token_type",
1670                    JWT_TOKEN_TYPE
1671                )))),
1672                request::body(url_decoded(contains(("audience", "audience")))),
1673                request::body(url_decoded(contains(("scope", DEFAULT_SCOPE)))),
1674            ])
1675            .respond_with(json_encoded(json!({
1676                "access_token": "sts-only-token",
1677                "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1678                "token_type": "Bearer",
1679                "expires_in": 3600,
1680            }))),
1681        );
1682
1683        let creds = Builder::new(contents)
1684            .build_access_token_credentials()
1685            .unwrap();
1686        let access_token = creds.access_token().await.unwrap();
1687        assert_eq!(access_token.token, "sts-only-token");
1688    }
1689
1690    #[tokio::test]
1691    async fn test_impersonation_flow_sts_call_fails() {
1692        let subject_token_server = Server::run();
1693        let sts_server = Server::run();
1694        let impersonation_server = Server::run();
1695
1696        let impersonation_path = "/projects/-/serviceAccounts/sa@test.com:generateAccessToken";
1697        let contents = json!({
1698            "type": "external_account",
1699            "audience": "audience",
1700            "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1701            "token_url": sts_server.url("/token").to_string(),
1702            "service_account_impersonation_url": impersonation_server.url(impersonation_path).to_string(),
1703            "credential_source": {
1704                "url": subject_token_server.url("/subject_token").to_string(),
1705                "format": {
1706                  "type": "json",
1707                  "subject_token_field_name": "access_token"
1708                }
1709            }
1710        });
1711
1712        subject_token_server.expect(
1713            Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1714                json_encoded(json!({
1715                    "access_token": "subject_token",
1716                })),
1717            ),
1718        );
1719
1720        sts_server.expect(
1721            Expectation::matching(request::method_path("POST", "/token"))
1722                .respond_with(status_code(500)),
1723        );
1724
1725        let creds = Builder::new(contents).build().unwrap();
1726        let err = creds.headers(Extensions::new()).await.unwrap_err();
1727        let original_err = find_source_error::<CredentialsError>(&err).unwrap();
1728        assert!(
1729            original_err
1730                .to_string()
1731                .contains("failed to exchange token")
1732        );
1733        assert!(original_err.is_transient());
1734    }
1735
1736    #[tokio::test]
1737    async fn test_impersonation_flow_iam_call_fails() {
1738        let subject_token_server = Server::run();
1739        let sts_server = Server::run();
1740        let impersonation_server = Server::run();
1741
1742        let impersonation_path = "/projects/-/serviceAccounts/sa@test.com:generateAccessToken";
1743        let contents = json!({
1744            "type": "external_account",
1745            "audience": "audience",
1746            "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1747            "token_url": sts_server.url("/token").to_string(),
1748            "service_account_impersonation_url": impersonation_server.url(impersonation_path).to_string(),
1749            "credential_source": {
1750                "url": subject_token_server.url("/subject_token").to_string(),
1751                "format": {
1752                  "type": "json",
1753                  "subject_token_field_name": "access_token"
1754                }
1755            }
1756        });
1757
1758        subject_token_server.expect(
1759            Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1760                json_encoded(json!({
1761                    "access_token": "subject_token",
1762                })),
1763            ),
1764        );
1765
1766        sts_server.expect(
1767            Expectation::matching(request::method_path("POST", "/token")).respond_with(
1768                json_encoded(json!({
1769                    "access_token": "sts-token",
1770                    "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1771                    "token_type": "Bearer",
1772                    "expires_in": 3600,
1773                })),
1774            ),
1775        );
1776
1777        impersonation_server.expect(
1778            Expectation::matching(request::method_path("POST", impersonation_path))
1779                .respond_with(status_code(403)),
1780        );
1781
1782        let creds = Builder::new(contents).build().unwrap();
1783        let err = creds.headers(Extensions::new()).await.unwrap_err();
1784        let original_err = find_source_error::<CredentialsError>(&err).unwrap();
1785        assert!(original_err.to_string().contains("failed to fetch token"));
1786        assert!(!original_err.is_transient());
1787    }
1788
1789    #[test_case(Some(vec!["scope1", "scope2"]), Some("http://custom.com/token") ; "with custom scopes and token_url")]
1790    #[test_case(None, Some("http://custom.com/token") ; "with default scopes and custom token_url")]
1791    #[test_case(Some(vec!["scope1", "scope2"]), None ; "with custom scopes and default token_url")]
1792    #[test_case(None, None ; "with default scopes and default token_url")]
1793    #[tokio::test]
1794    async fn create_programmatic_builder(scopes: Option<Vec<&str>>, token_url: Option<&str>) {
1795        let provider = Arc::new(TestSubjectTokenProvider);
1796        let mut builder = ProgrammaticBuilder::new(provider)
1797            .with_audience("test-audience")
1798            .with_subject_token_type("test-token-type")
1799            .with_client_id("test-client-id")
1800            .with_client_secret("test-client-secret")
1801            .with_target_principal("test-principal");
1802
1803        let expected_scopes = if let Some(scopes) = scopes.clone() {
1804            scopes.iter().map(|s| s.to_string()).collect()
1805        } else {
1806            vec![DEFAULT_SCOPE.to_string()]
1807        };
1808
1809        let expected_token_url = token_url.unwrap_or(STS_TOKEN_URL).to_string();
1810
1811        if let Some(scopes) = scopes {
1812            builder = builder.with_scopes(scopes);
1813        }
1814        if let Some(token_url) = token_url {
1815            builder = builder.with_token_url(token_url);
1816        }
1817
1818        let (config, _, _) = builder.build_components().unwrap();
1819
1820        assert_eq!(config.audience, "test-audience");
1821        assert_eq!(config.subject_token_type, "test-token-type");
1822        assert_eq!(config.client_id, Some("test-client-id".to_string()));
1823        assert_eq!(config.client_secret, Some("test-client-secret".to_string()));
1824        assert_eq!(config.scopes, expected_scopes);
1825        assert_eq!(config.token_url, expected_token_url);
1826        assert_eq!(
1827            config.service_account_impersonation_url,
1828            Some("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string())
1829        );
1830    }
1831
1832    #[tokio::test]
1833    async fn create_programmatic_builder_with_quota_project_id() {
1834        let provider = Arc::new(TestSubjectTokenProvider);
1835        let builder = ProgrammaticBuilder::new(provider)
1836            .with_audience("test-audience")
1837            .with_subject_token_type("test-token-type")
1838            .with_token_url(STS_TOKEN_URL)
1839            .with_quota_project_id("test-quota-project");
1840
1841        let creds = builder.build().unwrap();
1842
1843        let fmt = format!("{creds:?}");
1844        assert!(
1845            fmt.contains("ExternalAccountCredentials"),
1846            "Expected 'ExternalAccountCredentials', got: {fmt}"
1847        );
1848        assert!(
1849            fmt.contains("test-quota-project"),
1850            "Expected 'test-quota-project', got: {fmt}"
1851        );
1852    }
1853
1854    #[tokio::test]
1855    async fn programmatic_builder_returns_correct_headers() {
1856        let provider = Arc::new(TestSubjectTokenProvider);
1857        let sts_server = Server::run();
1858        let builder = ProgrammaticBuilder::new(provider)
1859            .with_audience("test-audience")
1860            .with_subject_token_type("test-token-type")
1861            .with_token_url(sts_server.url("/token").to_string())
1862            .with_quota_project_id("test-quota-project");
1863
1864        let creds = builder.build().unwrap();
1865
1866        sts_server.expect(
1867            Expectation::matching(all_of![
1868                request::method_path("POST", "/token"),
1869                request::body(url_decoded(contains((
1870                    "grant_type",
1871                    TOKEN_EXCHANGE_GRANT_TYPE
1872                )))),
1873                request::body(url_decoded(contains((
1874                    "subject_token",
1875                    "test-subject-token"
1876                )))),
1877                request::body(url_decoded(contains((
1878                    "requested_token_type",
1879                    ACCESS_TOKEN_TYPE
1880                )))),
1881                request::body(url_decoded(contains((
1882                    "subject_token_type",
1883                    "test-token-type"
1884                )))),
1885                request::body(url_decoded(contains(("audience", "test-audience")))),
1886                request::body(url_decoded(contains(("scope", DEFAULT_SCOPE)))),
1887            ])
1888            .respond_with(json_encoded(json!({
1889                "access_token": "sts-only-token",
1890                "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1891                "token_type": "Bearer",
1892                "expires_in": 3600,
1893            }))),
1894        );
1895
1896        let headers = creds.headers(Extensions::new()).await.unwrap();
1897        match headers {
1898            CacheableResource::New { data, .. } => {
1899                let token = data.get("authorization").unwrap().to_str().unwrap();
1900                assert_eq!(token, "Bearer sts-only-token");
1901                let quota_project = data.get("x-goog-user-project").unwrap().to_str().unwrap();
1902                assert_eq!(quota_project, "test-quota-project");
1903            }
1904            CacheableResource::NotModified => panic!("Expected new headers"),
1905        }
1906    }
1907
1908    #[tokio::test]
1909    async fn create_programmatic_builder_fails_on_missing_required_field() {
1910        let provider = Arc::new(TestSubjectTokenProvider);
1911        let result = ProgrammaticBuilder::new(provider)
1912            .with_subject_token_type("test-token-type")
1913            // Missing .with_audience(...)
1914            .with_token_url("http://test.com/token")
1915            .build();
1916
1917        assert!(result.is_err(), "{result:?}");
1918        let error_string = result.unwrap_err().to_string();
1919        assert!(
1920            error_string.contains("missing required field: audience"),
1921            "Expected error about missing 'audience', got: {error_string}"
1922        );
1923    }
1924
1925    #[tokio::test]
1926    async fn test_external_account_retries_on_transient_failures() {
1927        let mut subject_token_server = Server::run();
1928        let mut sts_server = Server::run();
1929
1930        let contents = json!({
1931            "type": "external_account",
1932            "audience": "audience",
1933            "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1934            "token_url": sts_server.url("/token").to_string(),
1935            "credential_source": {
1936                "url": subject_token_server.url("/subject_token").to_string(),
1937            }
1938        });
1939
1940        subject_token_server.expect(
1941            Expectation::matching(request::method_path("GET", "/subject_token"))
1942                .times(3)
1943                .respond_with(json_encoded(json!({
1944                    "access_token": "subject_token",
1945                }))),
1946        );
1947
1948        sts_server.expect(
1949            Expectation::matching(request::method_path("POST", "/token"))
1950                .times(3)
1951                .respond_with(status_code(503)),
1952        );
1953
1954        let creds = Builder::new(contents)
1955            .with_retry_policy(get_mock_auth_retry_policy(3))
1956            .with_backoff_policy(get_mock_backoff_policy())
1957            .with_retry_throttler(get_mock_retry_throttler())
1958            .build()
1959            .unwrap();
1960
1961        let err = creds.headers(Extensions::new()).await.unwrap_err();
1962        assert!(err.is_transient(), "{err:?}");
1963        sts_server.verify_and_clear();
1964        subject_token_server.verify_and_clear();
1965    }
1966
1967    #[tokio::test]
1968    async fn test_external_account_does_not_retry_on_non_transient_failures() {
1969        let subject_token_server = Server::run();
1970        let mut sts_server = Server::run();
1971
1972        let contents = json!({
1973            "type": "external_account",
1974            "audience": "audience",
1975            "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1976            "token_url": sts_server.url("/token").to_string(),
1977            "credential_source": {
1978                "url": subject_token_server.url("/subject_token").to_string(),
1979            }
1980        });
1981
1982        subject_token_server.expect(
1983            Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1984                json_encoded(json!({
1985                    "access_token": "subject_token",
1986                })),
1987            ),
1988        );
1989
1990        sts_server.expect(
1991            Expectation::matching(request::method_path("POST", "/token"))
1992                .times(1)
1993                .respond_with(status_code(401)),
1994        );
1995
1996        let creds = Builder::new(contents)
1997            .with_retry_policy(get_mock_auth_retry_policy(1))
1998            .with_backoff_policy(get_mock_backoff_policy())
1999            .with_retry_throttler(get_mock_retry_throttler())
2000            .build()
2001            .unwrap();
2002
2003        let err = creds.headers(Extensions::new()).await.unwrap_err();
2004        assert!(!err.is_transient());
2005        sts_server.verify_and_clear();
2006    }
2007
2008    #[tokio::test]
2009    async fn test_external_account_retries_for_success() {
2010        let mut subject_token_server = Server::run();
2011        let mut sts_server = Server::run();
2012
2013        let contents = json!({
2014            "type": "external_account",
2015            "audience": "audience",
2016            "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
2017            "token_url": sts_server.url("/token").to_string(),
2018            "credential_source": {
2019                "url": subject_token_server.url("/subject_token").to_string(),
2020            }
2021        });
2022
2023        subject_token_server.expect(
2024            Expectation::matching(request::method_path("GET", "/subject_token"))
2025                .times(3)
2026                .respond_with(json_encoded(json!({
2027                    "access_token": "subject_token",
2028                }))),
2029        );
2030
2031        sts_server.expect(
2032            Expectation::matching(request::method_path("POST", "/token"))
2033                .times(3)
2034                .respond_with(cycle![
2035                    status_code(503).body("try-again"),
2036                    status_code(503).body("try-again"),
2037                    json_encoded(json!({
2038                        "access_token": "sts-only-token",
2039                        "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
2040                        "token_type": "Bearer",
2041                        "expires_in": 3600,
2042                    }))
2043                ]),
2044        );
2045
2046        let creds = Builder::new(contents)
2047            .with_retry_policy(get_mock_auth_retry_policy(3))
2048            .with_backoff_policy(get_mock_backoff_policy())
2049            .with_retry_throttler(get_mock_retry_throttler())
2050            .build()
2051            .unwrap();
2052
2053        let headers = creds.headers(Extensions::new()).await.unwrap();
2054        match headers {
2055            CacheableResource::New { data, .. } => {
2056                let token = data.get("authorization").unwrap().to_str().unwrap();
2057                assert_eq!(token, "Bearer sts-only-token");
2058            }
2059            CacheableResource::NotModified => panic!("Expected new headers"),
2060        }
2061        sts_server.verify_and_clear();
2062        subject_token_server.verify_and_clear();
2063    }
2064
2065    #[tokio::test]
2066    async fn test_programmatic_builder_retries_on_transient_failures() {
2067        let provider = Arc::new(TestSubjectTokenProvider);
2068        let mut sts_server = Server::run();
2069
2070        sts_server.expect(
2071            Expectation::matching(request::method_path("POST", "/token"))
2072                .times(3)
2073                .respond_with(status_code(503)),
2074        );
2075
2076        let creds = ProgrammaticBuilder::new(provider)
2077            .with_audience("test-audience")
2078            .with_subject_token_type("test-token-type")
2079            .with_token_url(sts_server.url("/token").to_string())
2080            .with_retry_policy(get_mock_auth_retry_policy(3))
2081            .with_backoff_policy(get_mock_backoff_policy())
2082            .with_retry_throttler(get_mock_retry_throttler())
2083            .build()
2084            .unwrap();
2085
2086        let err = creds.headers(Extensions::new()).await.unwrap_err();
2087        assert!(err.is_transient(), "{err:?}");
2088        sts_server.verify_and_clear();
2089    }
2090
2091    #[tokio::test]
2092    async fn test_programmatic_builder_does_not_retry_on_non_transient_failures() {
2093        let provider = Arc::new(TestSubjectTokenProvider);
2094        let mut sts_server = Server::run();
2095
2096        sts_server.expect(
2097            Expectation::matching(request::method_path("POST", "/token"))
2098                .times(1)
2099                .respond_with(status_code(401)),
2100        );
2101
2102        let creds = ProgrammaticBuilder::new(provider)
2103            .with_audience("test-audience")
2104            .with_subject_token_type("test-token-type")
2105            .with_token_url(sts_server.url("/token").to_string())
2106            .with_retry_policy(get_mock_auth_retry_policy(1))
2107            .with_backoff_policy(get_mock_backoff_policy())
2108            .with_retry_throttler(get_mock_retry_throttler())
2109            .build()
2110            .unwrap();
2111
2112        let err = creds.headers(Extensions::new()).await.unwrap_err();
2113        assert!(!err.is_transient());
2114        sts_server.verify_and_clear();
2115    }
2116
2117    #[tokio::test]
2118    async fn test_programmatic_builder_retries_for_success() {
2119        let provider = Arc::new(TestSubjectTokenProvider);
2120        let mut sts_server = Server::run();
2121
2122        sts_server.expect(
2123            Expectation::matching(request::method_path("POST", "/token"))
2124                .times(3)
2125                .respond_with(cycle![
2126                    status_code(503).body("try-again"),
2127                    status_code(503).body("try-again"),
2128                    json_encoded(json!({
2129                        "access_token": "sts-only-token",
2130                        "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
2131                        "token_type": "Bearer",
2132                        "expires_in": 3600,
2133                    }))
2134                ]),
2135        );
2136
2137        let creds = ProgrammaticBuilder::new(provider)
2138            .with_audience("test-audience")
2139            .with_subject_token_type("test-token-type")
2140            .with_token_url(sts_server.url("/token").to_string())
2141            .with_retry_policy(get_mock_auth_retry_policy(3))
2142            .with_backoff_policy(get_mock_backoff_policy())
2143            .with_retry_throttler(get_mock_retry_throttler())
2144            .build()
2145            .unwrap();
2146
2147        let headers = creds.headers(Extensions::new()).await.unwrap();
2148        match headers {
2149            CacheableResource::New { data, .. } => {
2150                let token = data.get("authorization").unwrap().to_str().unwrap();
2151                assert_eq!(token, "Bearer sts-only-token");
2152            }
2153            CacheableResource::NotModified => panic!("Expected new headers"),
2154        }
2155        sts_server.verify_and_clear();
2156    }
2157
2158    #[tokio::test]
2159    async fn test_kubernetes_wif_direct_identity_parsing() {
2160        let contents = json!({
2161            "audience": "//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/pool-name/providers/k8s-cluster",
2162            "credential_source": {
2163                "file": "/var/run/service-account/token"
2164            },
2165            "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
2166            "token_url": "https://sts.googleapis.com/v1/token",
2167            "type": "external_account"
2168        });
2169
2170        let file: ExternalAccountFile = serde_json::from_value(contents)
2171            .expect("failed to parse kubernetes WIF direct identity config");
2172        let config: ExternalAccountConfig = file.into();
2173
2174        match config.credential_source {
2175            CredentialSource::File(source) => {
2176                assert_eq!(source.file, "/var/run/service-account/token");
2177                assert_eq!(source.format, "text"); // Default format
2178                assert_eq!(source.subject_token_field_name, ""); // Default empty
2179            }
2180            _ => {
2181                unreachable!("expected File sourced credential")
2182            }
2183        }
2184    }
2185
2186    #[tokio::test]
2187    async fn test_kubernetes_wif_impersonation_parsing() {
2188        let contents = json!({
2189            "audience": "//iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/pool-name/providers/k8s-cluster",
2190            "credential_source": {
2191                "file": "/var/run/service-account/token",
2192                "format": {
2193                    "type": "text"
2194                }
2195            },
2196            "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-sa@test-project.iam.gserviceaccount.com:generateAccessToken",
2197            "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
2198            "token_url": "https://sts.googleapis.com/v1/token",
2199            "type": "external_account",
2200            "universe_domain": "googleapis.com"
2201        });
2202
2203        let file: ExternalAccountFile = serde_json::from_value(contents)
2204            .expect("failed to parse kubernetes WIF impersonation config");
2205        let config: ExternalAccountConfig = file.into();
2206
2207        match config.credential_source {
2208            CredentialSource::File(source) => {
2209                assert_eq!(source.file, "/var/run/service-account/token");
2210                assert_eq!(source.format, "text");
2211                assert_eq!(source.subject_token_field_name, ""); // Empty for text format
2212            }
2213            _ => {
2214                unreachable!("expected File sourced credential")
2215            }
2216        }
2217
2218        assert_eq!(
2219            config.service_account_impersonation_url,
2220            Some("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-sa@test-project.iam.gserviceaccount.com:generateAccessToken".to_string())
2221        );
2222    }
2223}