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