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