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