google_cloud_auth/credentials/
external_account.rs

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