Skip to main content

google_cloud_auth/credentials/
external_account.rs

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