Skip to main content

google_cloud_auth/credentials/
external_account.rs

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