Skip to main content

google_cloud_auth/credentials/
external_account.rs

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