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 get_mock_auth_retry_policy, get_mock_backoff_policy, get_mock_retry_throttler,
1273 };
1274 use crate::errors::SubjectTokenProviderError;
1275 use httptest::{
1276 Expectation, Server, cycle,
1277 matchers::{all_of, contains, request, url_decoded},
1278 responders::{json_encoded, status_code},
1279 };
1280 use serde_json::*;
1281 use std::collections::HashMap;
1282 use std::error::Error;
1283 use std::fmt;
1284 use test_case::test_case;
1285 use time::OffsetDateTime;
1286
1287 #[derive(Debug)]
1288 struct TestProviderError;
1289 impl fmt::Display for TestProviderError {
1290 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1291 write!(f, "TestProviderError")
1292 }
1293 }
1294 impl Error for TestProviderError {}
1295 impl SubjectTokenProviderError for TestProviderError {
1296 fn is_transient(&self) -> bool {
1297 false
1298 }
1299 }
1300
1301 #[derive(Debug)]
1302 struct TestSubjectTokenProvider;
1303 impl SubjectTokenProvider for TestSubjectTokenProvider {
1304 type Error = TestProviderError;
1305 async fn subject_token(&self) -> std::result::Result<SubjectToken, Self::Error> {
1306 Ok(SubjectTokenBuilder::new("test-subject-token".to_string()).build())
1307 }
1308 }
1309
1310 #[tokio::test]
1311 async fn create_external_account_builder() {
1312 let contents = json!({
1313 "type": "external_account",
1314 "audience": "audience",
1315 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1316 "token_url": "https://sts.googleapis.com/v1beta/token",
1317 "credential_source": {
1318 "url": "https://example.com/token",
1319 "format": {
1320 "type": "json",
1321 "subject_token_field_name": "access_token"
1322 }
1323 }
1324 });
1325
1326 let creds = Builder::new(contents)
1327 .with_quota_project_id("test_project")
1328 .with_scopes(["a", "b"])
1329 .build()
1330 .unwrap();
1331
1332 // Use the debug output to verify the right kind of credentials are created.
1333 let fmt = format!("{creds:?}");
1334 assert!(fmt.contains("ExternalAccountCredentials"));
1335 }
1336
1337 #[tokio::test]
1338 async fn create_external_account_detect_url_sourced() {
1339 let contents = json!({
1340 "type": "external_account",
1341 "audience": "audience",
1342 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1343 "token_url": "https://sts.googleapis.com/v1beta/token",
1344 "credential_source": {
1345 "url": "https://example.com/token",
1346 "headers": {
1347 "Metadata": "True"
1348 },
1349 "format": {
1350 "type": "json",
1351 "subject_token_field_name": "access_token"
1352 }
1353 }
1354 });
1355
1356 let file: ExternalAccountFile =
1357 serde_json::from_value(contents).expect("failed to parse external account config");
1358 let config: ExternalAccountConfig = file.into();
1359 let source = config.credential_source;
1360
1361 match source {
1362 CredentialSource::Url(source) => {
1363 assert_eq!(source.url, "https://example.com/token");
1364 assert_eq!(
1365 source.headers,
1366 HashMap::from([("Metadata".to_string(), "True".to_string()),]),
1367 );
1368 assert_eq!(source.format, "json");
1369 assert_eq!(source.subject_token_field_name, "access_token");
1370 }
1371 _ => {
1372 unreachable!("expected Url Sourced credential")
1373 }
1374 }
1375 }
1376
1377 #[tokio::test]
1378 async fn create_external_account_detect_executable_sourced() {
1379 let contents = json!({
1380 "type": "external_account",
1381 "audience": "audience",
1382 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1383 "token_url": "https://sts.googleapis.com/v1beta/token",
1384 "credential_source": {
1385 "executable": {
1386 "command": "cat /some/file",
1387 "output_file": "/some/file",
1388 "timeout_millis": 5000
1389 }
1390 }
1391 });
1392
1393 let file: ExternalAccountFile =
1394 serde_json::from_value(contents).expect("failed to parse external account config");
1395 let config: ExternalAccountConfig = file.into();
1396 let source = config.credential_source;
1397
1398 match source {
1399 CredentialSource::Executable(source) => {
1400 assert_eq!(source.command, "cat");
1401 assert_eq!(source.args, vec!["/some/file"]);
1402 assert_eq!(source.output_file.as_deref(), Some("/some/file"));
1403 assert_eq!(source.timeout, Duration::from_secs(5));
1404 }
1405 _ => {
1406 unreachable!("expected Executable Sourced credential")
1407 }
1408 }
1409 }
1410
1411 #[tokio::test]
1412 async fn create_external_account_detect_file_sourced() {
1413 let contents = json!({
1414 "type": "external_account",
1415 "audience": "audience",
1416 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1417 "token_url": "https://sts.googleapis.com/v1beta/token",
1418 "credential_source": {
1419 "file": "/foo/bar",
1420 "format": {
1421 "type": "json",
1422 "subject_token_field_name": "token"
1423 }
1424 }
1425 });
1426
1427 let file: ExternalAccountFile =
1428 serde_json::from_value(contents).expect("failed to parse external account config");
1429 let config: ExternalAccountConfig = file.into();
1430 let source = config.credential_source;
1431
1432 match source {
1433 CredentialSource::File(source) => {
1434 assert_eq!(source.file, "/foo/bar");
1435 assert_eq!(source.format, "json");
1436 assert_eq!(source.subject_token_field_name, "token");
1437 }
1438 _ => {
1439 unreachable!("expected File Sourced credential")
1440 }
1441 }
1442 }
1443
1444 #[tokio::test]
1445 async fn test_external_account_with_impersonation_success() {
1446 let subject_token_server = Server::run();
1447 let sts_server = Server::run();
1448 let impersonation_server = Server::run();
1449
1450 let impersonation_path = "/projects/-/serviceAccounts/sa@test.com:generateAccessToken";
1451 let contents = json!({
1452 "type": "external_account",
1453 "audience": "audience",
1454 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1455 "token_url": sts_server.url("/token").to_string(),
1456 "service_account_impersonation_url": impersonation_server.url(impersonation_path).to_string(),
1457 "credential_source": {
1458 "url": subject_token_server.url("/subject_token").to_string(),
1459 "format": {
1460 "type": "json",
1461 "subject_token_field_name": "access_token"
1462 }
1463 }
1464 });
1465
1466 subject_token_server.expect(
1467 Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1468 json_encoded(json!({
1469 "access_token": "subject_token",
1470 })),
1471 ),
1472 );
1473
1474 sts_server.expect(
1475 Expectation::matching(all_of![
1476 request::method_path("POST", "/token"),
1477 request::body(url_decoded(contains((
1478 "grant_type",
1479 TOKEN_EXCHANGE_GRANT_TYPE
1480 )))),
1481 request::body(url_decoded(contains(("subject_token", "subject_token")))),
1482 request::body(url_decoded(contains((
1483 "requested_token_type",
1484 ACCESS_TOKEN_TYPE
1485 )))),
1486 request::body(url_decoded(contains((
1487 "subject_token_type",
1488 JWT_TOKEN_TYPE
1489 )))),
1490 request::body(url_decoded(contains(("audience", "audience")))),
1491 request::body(url_decoded(contains(("scope", IAM_SCOPE)))),
1492 ])
1493 .respond_with(json_encoded(json!({
1494 "access_token": "sts-token",
1495 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1496 "token_type": "Bearer",
1497 "expires_in": 3600,
1498 }))),
1499 );
1500
1501 let expire_time = (OffsetDateTime::now_utc() + time::Duration::hours(1))
1502 .format(&time::format_description::well_known::Rfc3339)
1503 .unwrap();
1504 impersonation_server.expect(
1505 Expectation::matching(all_of![
1506 request::method_path("POST", impersonation_path),
1507 request::headers(contains(("authorization", "Bearer sts-token"))),
1508 ])
1509 .respond_with(json_encoded(json!({
1510 "accessToken": "final-impersonated-token",
1511 "expireTime": expire_time
1512 }))),
1513 );
1514
1515 let creds = Builder::new(contents).build().unwrap();
1516 let headers = creds.headers(Extensions::new()).await.unwrap();
1517 match headers {
1518 CacheableResource::New { data, .. } => {
1519 let token = data.get("authorization").unwrap().to_str().unwrap();
1520 assert_eq!(token, "Bearer final-impersonated-token");
1521 }
1522 CacheableResource::NotModified => panic!("Expected new headers"),
1523 }
1524 }
1525
1526 #[tokio::test]
1527 async fn test_external_account_without_impersonation_success() {
1528 let subject_token_server = Server::run();
1529 let sts_server = Server::run();
1530
1531 let contents = json!({
1532 "type": "external_account",
1533 "audience": "audience",
1534 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1535 "token_url": sts_server.url("/token").to_string(),
1536 "credential_source": {
1537 "url": subject_token_server.url("/subject_token").to_string(),
1538 "format": {
1539 "type": "json",
1540 "subject_token_field_name": "access_token"
1541 }
1542 }
1543 });
1544
1545 subject_token_server.expect(
1546 Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1547 json_encoded(json!({
1548 "access_token": "subject_token",
1549 })),
1550 ),
1551 );
1552
1553 sts_server.expect(
1554 Expectation::matching(all_of![
1555 request::method_path("POST", "/token"),
1556 request::body(url_decoded(contains((
1557 "grant_type",
1558 TOKEN_EXCHANGE_GRANT_TYPE
1559 )))),
1560 request::body(url_decoded(contains(("subject_token", "subject_token")))),
1561 request::body(url_decoded(contains((
1562 "requested_token_type",
1563 ACCESS_TOKEN_TYPE
1564 )))),
1565 request::body(url_decoded(contains((
1566 "subject_token_type",
1567 JWT_TOKEN_TYPE
1568 )))),
1569 request::body(url_decoded(contains(("audience", "audience")))),
1570 request::body(url_decoded(contains(("scope", DEFAULT_SCOPE)))),
1571 ])
1572 .respond_with(json_encoded(json!({
1573 "access_token": "sts-only-token",
1574 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1575 "token_type": "Bearer",
1576 "expires_in": 3600,
1577 }))),
1578 );
1579
1580 let creds = Builder::new(contents).build().unwrap();
1581 let headers = creds.headers(Extensions::new()).await.unwrap();
1582 match headers {
1583 CacheableResource::New { data, .. } => {
1584 let token = data.get("authorization").unwrap().to_str().unwrap();
1585 assert_eq!(token, "Bearer sts-only-token");
1586 }
1587 CacheableResource::NotModified => panic!("Expected new headers"),
1588 }
1589 }
1590
1591 #[tokio::test]
1592 async fn test_impersonation_flow_sts_call_fails() {
1593 let subject_token_server = Server::run();
1594 let sts_server = Server::run();
1595 let impersonation_server = Server::run();
1596
1597 let impersonation_path = "/projects/-/serviceAccounts/sa@test.com:generateAccessToken";
1598 let contents = json!({
1599 "type": "external_account",
1600 "audience": "audience",
1601 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1602 "token_url": sts_server.url("/token").to_string(),
1603 "service_account_impersonation_url": impersonation_server.url(impersonation_path).to_string(),
1604 "credential_source": {
1605 "url": subject_token_server.url("/subject_token").to_string(),
1606 "format": {
1607 "type": "json",
1608 "subject_token_field_name": "access_token"
1609 }
1610 }
1611 });
1612
1613 subject_token_server.expect(
1614 Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1615 json_encoded(json!({
1616 "access_token": "subject_token",
1617 })),
1618 ),
1619 );
1620
1621 sts_server.expect(
1622 Expectation::matching(request::method_path("POST", "/token"))
1623 .respond_with(status_code(500)),
1624 );
1625
1626 let creds = Builder::new(contents).build().unwrap();
1627 let err = creds.headers(Extensions::new()).await.unwrap_err();
1628 assert!(err.to_string().contains("failed to exchange token"));
1629 assert!(err.is_transient());
1630 }
1631
1632 #[tokio::test]
1633 async fn test_impersonation_flow_iam_call_fails() {
1634 let subject_token_server = Server::run();
1635 let sts_server = Server::run();
1636 let impersonation_server = Server::run();
1637
1638 let impersonation_path = "/projects/-/serviceAccounts/sa@test.com:generateAccessToken";
1639 let contents = json!({
1640 "type": "external_account",
1641 "audience": "audience",
1642 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1643 "token_url": sts_server.url("/token").to_string(),
1644 "service_account_impersonation_url": impersonation_server.url(impersonation_path).to_string(),
1645 "credential_source": {
1646 "url": subject_token_server.url("/subject_token").to_string(),
1647 "format": {
1648 "type": "json",
1649 "subject_token_field_name": "access_token"
1650 }
1651 }
1652 });
1653
1654 subject_token_server.expect(
1655 Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1656 json_encoded(json!({
1657 "access_token": "subject_token",
1658 })),
1659 ),
1660 );
1661
1662 sts_server.expect(
1663 Expectation::matching(request::method_path("POST", "/token")).respond_with(
1664 json_encoded(json!({
1665 "access_token": "sts-token",
1666 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1667 "token_type": "Bearer",
1668 "expires_in": 3600,
1669 })),
1670 ),
1671 );
1672
1673 impersonation_server.expect(
1674 Expectation::matching(request::method_path("POST", impersonation_path))
1675 .respond_with(status_code(403)),
1676 );
1677
1678 let creds = Builder::new(contents).build().unwrap();
1679 let err = creds.headers(Extensions::new()).await.unwrap_err();
1680 assert!(err.to_string().contains("failed to fetch token"));
1681 assert!(!err.is_transient());
1682 }
1683
1684 #[test_case(Some(vec!["scope1", "scope2"]), Some("http://custom.com/token") ; "with custom scopes and token_url")]
1685 #[test_case(None, Some("http://custom.com/token") ; "with default scopes and custom token_url")]
1686 #[test_case(Some(vec!["scope1", "scope2"]), None ; "with custom scopes and default token_url")]
1687 #[test_case(None, None ; "with default scopes and default token_url")]
1688 #[tokio::test]
1689 async fn create_programmatic_builder(scopes: Option<Vec<&str>>, token_url: Option<&str>) {
1690 let provider = Arc::new(TestSubjectTokenProvider);
1691 let mut builder = ProgrammaticBuilder::new(provider)
1692 .with_audience("test-audience")
1693 .with_subject_token_type("test-token-type")
1694 .with_client_id("test-client-id")
1695 .with_client_secret("test-client-secret")
1696 .with_target_principal("test-principal");
1697
1698 let expected_scopes = if let Some(scopes) = scopes.clone() {
1699 scopes.iter().map(|s| s.to_string()).collect()
1700 } else {
1701 vec![DEFAULT_SCOPE.to_string()]
1702 };
1703
1704 let expected_token_url = token_url.unwrap_or(STS_TOKEN_URL).to_string();
1705
1706 if let Some(scopes) = scopes {
1707 builder = builder.with_scopes(scopes);
1708 }
1709 if let Some(token_url) = token_url {
1710 builder = builder.with_token_url(token_url);
1711 }
1712
1713 let (config, _, _) = builder.build_components().unwrap();
1714
1715 assert_eq!(config.audience, "test-audience");
1716 assert_eq!(config.subject_token_type, "test-token-type");
1717 assert_eq!(config.client_id, Some("test-client-id".to_string()));
1718 assert_eq!(config.client_secret, Some("test-client-secret".to_string()));
1719 assert_eq!(config.scopes, expected_scopes);
1720 assert_eq!(config.token_url, expected_token_url);
1721 assert_eq!(
1722 config.service_account_impersonation_url,
1723 Some("https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test-principal:generateAccessToken".to_string())
1724 );
1725 }
1726
1727 #[tokio::test]
1728 async fn create_programmatic_builder_with_quota_project_id() {
1729 let provider = Arc::new(TestSubjectTokenProvider);
1730 let builder = ProgrammaticBuilder::new(provider)
1731 .with_audience("test-audience")
1732 .with_subject_token_type("test-token-type")
1733 .with_token_url(STS_TOKEN_URL)
1734 .with_quota_project_id("test-quota-project");
1735
1736 let creds = builder.build().unwrap();
1737
1738 let fmt = format!("{creds:?}");
1739 assert!(
1740 fmt.contains("ExternalAccountCredentials"),
1741 "Expected 'ExternalAccountCredentials', got: {fmt}"
1742 );
1743 assert!(
1744 fmt.contains("test-quota-project"),
1745 "Expected 'test-quota-project', got: {fmt}"
1746 );
1747 }
1748
1749 #[tokio::test]
1750 async fn programmatic_builder_returns_correct_headers() {
1751 let provider = Arc::new(TestSubjectTokenProvider);
1752 let sts_server = Server::run();
1753 let builder = ProgrammaticBuilder::new(provider)
1754 .with_audience("test-audience")
1755 .with_subject_token_type("test-token-type")
1756 .with_token_url(sts_server.url("/token").to_string())
1757 .with_quota_project_id("test-quota-project");
1758
1759 let creds = builder.build().unwrap();
1760
1761 sts_server.expect(
1762 Expectation::matching(all_of![
1763 request::method_path("POST", "/token"),
1764 request::body(url_decoded(contains((
1765 "grant_type",
1766 TOKEN_EXCHANGE_GRANT_TYPE
1767 )))),
1768 request::body(url_decoded(contains((
1769 "subject_token",
1770 "test-subject-token"
1771 )))),
1772 request::body(url_decoded(contains((
1773 "requested_token_type",
1774 ACCESS_TOKEN_TYPE
1775 )))),
1776 request::body(url_decoded(contains((
1777 "subject_token_type",
1778 "test-token-type"
1779 )))),
1780 request::body(url_decoded(contains(("audience", "test-audience")))),
1781 request::body(url_decoded(contains(("scope", DEFAULT_SCOPE)))),
1782 ])
1783 .respond_with(json_encoded(json!({
1784 "access_token": "sts-only-token",
1785 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1786 "token_type": "Bearer",
1787 "expires_in": 3600,
1788 }))),
1789 );
1790
1791 let headers = creds.headers(Extensions::new()).await.unwrap();
1792 match headers {
1793 CacheableResource::New { data, .. } => {
1794 let token = data.get("authorization").unwrap().to_str().unwrap();
1795 assert_eq!(token, "Bearer sts-only-token");
1796 let quota_project = data.get("x-goog-user-project").unwrap().to_str().unwrap();
1797 assert_eq!(quota_project, "test-quota-project");
1798 }
1799 CacheableResource::NotModified => panic!("Expected new headers"),
1800 }
1801 }
1802
1803 #[tokio::test]
1804 async fn create_programmatic_builder_fails_on_missing_required_field() {
1805 let provider = Arc::new(TestSubjectTokenProvider);
1806 let result = ProgrammaticBuilder::new(provider)
1807 .with_subject_token_type("test-token-type")
1808 // Missing .with_audience(...)
1809 .with_token_url("http://test.com/token")
1810 .build();
1811
1812 assert!(result.is_err());
1813 let error_string = result.unwrap_err().to_string();
1814 assert!(
1815 error_string.contains("missing required field: audience"),
1816 "Expected error about missing 'audience', got: {error_string}"
1817 );
1818 }
1819
1820 #[tokio::test]
1821 async fn test_external_account_retries_on_transient_failures() {
1822 let mut subject_token_server = Server::run();
1823 let mut sts_server = Server::run();
1824
1825 let contents = json!({
1826 "type": "external_account",
1827 "audience": "audience",
1828 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1829 "token_url": sts_server.url("/token").to_string(),
1830 "credential_source": {
1831 "url": subject_token_server.url("/subject_token").to_string(),
1832 }
1833 });
1834
1835 subject_token_server.expect(
1836 Expectation::matching(request::method_path("GET", "/subject_token"))
1837 .times(3)
1838 .respond_with(json_encoded(json!({
1839 "access_token": "subject_token",
1840 }))),
1841 );
1842
1843 sts_server.expect(
1844 Expectation::matching(request::method_path("POST", "/token"))
1845 .times(3)
1846 .respond_with(status_code(503)),
1847 );
1848
1849 let creds = Builder::new(contents)
1850 .with_retry_policy(get_mock_auth_retry_policy(3))
1851 .with_backoff_policy(get_mock_backoff_policy())
1852 .with_retry_throttler(get_mock_retry_throttler())
1853 .build()
1854 .unwrap();
1855
1856 let err = creds.headers(Extensions::new()).await.unwrap_err();
1857 assert!(err.is_transient());
1858 sts_server.verify_and_clear();
1859 subject_token_server.verify_and_clear();
1860 }
1861
1862 #[tokio::test]
1863 async fn test_external_account_does_not_retry_on_non_transient_failures() {
1864 let subject_token_server = Server::run();
1865 let mut sts_server = Server::run();
1866
1867 let contents = json!({
1868 "type": "external_account",
1869 "audience": "audience",
1870 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1871 "token_url": sts_server.url("/token").to_string(),
1872 "credential_source": {
1873 "url": subject_token_server.url("/subject_token").to_string(),
1874 }
1875 });
1876
1877 subject_token_server.expect(
1878 Expectation::matching(request::method_path("GET", "/subject_token")).respond_with(
1879 json_encoded(json!({
1880 "access_token": "subject_token",
1881 })),
1882 ),
1883 );
1884
1885 sts_server.expect(
1886 Expectation::matching(request::method_path("POST", "/token"))
1887 .times(1)
1888 .respond_with(status_code(401)),
1889 );
1890
1891 let creds = Builder::new(contents)
1892 .with_retry_policy(get_mock_auth_retry_policy(1))
1893 .with_backoff_policy(get_mock_backoff_policy())
1894 .with_retry_throttler(get_mock_retry_throttler())
1895 .build()
1896 .unwrap();
1897
1898 let err = creds.headers(Extensions::new()).await.unwrap_err();
1899 assert!(!err.is_transient());
1900 sts_server.verify_and_clear();
1901 }
1902
1903 #[tokio::test]
1904 async fn test_external_account_retries_for_success() {
1905 let mut subject_token_server = Server::run();
1906 let mut sts_server = Server::run();
1907
1908 let contents = json!({
1909 "type": "external_account",
1910 "audience": "audience",
1911 "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
1912 "token_url": sts_server.url("/token").to_string(),
1913 "credential_source": {
1914 "url": subject_token_server.url("/subject_token").to_string(),
1915 }
1916 });
1917
1918 subject_token_server.expect(
1919 Expectation::matching(request::method_path("GET", "/subject_token"))
1920 .times(3)
1921 .respond_with(json_encoded(json!({
1922 "access_token": "subject_token",
1923 }))),
1924 );
1925
1926 sts_server.expect(
1927 Expectation::matching(request::method_path("POST", "/token"))
1928 .times(3)
1929 .respond_with(cycle![
1930 status_code(503).body("try-again"),
1931 status_code(503).body("try-again"),
1932 json_encoded(json!({
1933 "access_token": "sts-only-token",
1934 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
1935 "token_type": "Bearer",
1936 "expires_in": 3600,
1937 }))
1938 ]),
1939 );
1940
1941 let creds = Builder::new(contents)
1942 .with_retry_policy(get_mock_auth_retry_policy(3))
1943 .with_backoff_policy(get_mock_backoff_policy())
1944 .with_retry_throttler(get_mock_retry_throttler())
1945 .build()
1946 .unwrap();
1947
1948 let headers = creds.headers(Extensions::new()).await.unwrap();
1949 match headers {
1950 CacheableResource::New { data, .. } => {
1951 let token = data.get("authorization").unwrap().to_str().unwrap();
1952 assert_eq!(token, "Bearer sts-only-token");
1953 }
1954 CacheableResource::NotModified => panic!("Expected new headers"),
1955 }
1956 sts_server.verify_and_clear();
1957 subject_token_server.verify_and_clear();
1958 }
1959
1960 #[tokio::test]
1961 async fn test_programmatic_builder_retries_on_transient_failures() {
1962 let provider = Arc::new(TestSubjectTokenProvider);
1963 let mut sts_server = Server::run();
1964
1965 sts_server.expect(
1966 Expectation::matching(request::method_path("POST", "/token"))
1967 .times(3)
1968 .respond_with(status_code(503)),
1969 );
1970
1971 let creds = ProgrammaticBuilder::new(provider)
1972 .with_audience("test-audience")
1973 .with_subject_token_type("test-token-type")
1974 .with_token_url(sts_server.url("/token").to_string())
1975 .with_retry_policy(get_mock_auth_retry_policy(3))
1976 .with_backoff_policy(get_mock_backoff_policy())
1977 .with_retry_throttler(get_mock_retry_throttler())
1978 .build()
1979 .unwrap();
1980
1981 let err = creds.headers(Extensions::new()).await.unwrap_err();
1982 assert!(err.is_transient());
1983 sts_server.verify_and_clear();
1984 }
1985
1986 #[tokio::test]
1987 async fn test_programmatic_builder_does_not_retry_on_non_transient_failures() {
1988 let provider = Arc::new(TestSubjectTokenProvider);
1989 let mut sts_server = Server::run();
1990
1991 sts_server.expect(
1992 Expectation::matching(request::method_path("POST", "/token"))
1993 .times(1)
1994 .respond_with(status_code(401)),
1995 );
1996
1997 let creds = ProgrammaticBuilder::new(provider)
1998 .with_audience("test-audience")
1999 .with_subject_token_type("test-token-type")
2000 .with_token_url(sts_server.url("/token").to_string())
2001 .with_retry_policy(get_mock_auth_retry_policy(1))
2002 .with_backoff_policy(get_mock_backoff_policy())
2003 .with_retry_throttler(get_mock_retry_throttler())
2004 .build()
2005 .unwrap();
2006
2007 let err = creds.headers(Extensions::new()).await.unwrap_err();
2008 assert!(!err.is_transient());
2009 sts_server.verify_and_clear();
2010 }
2011
2012 #[tokio::test]
2013 async fn test_programmatic_builder_retries_for_success() {
2014 let provider = Arc::new(TestSubjectTokenProvider);
2015 let mut sts_server = Server::run();
2016
2017 sts_server.expect(
2018 Expectation::matching(request::method_path("POST", "/token"))
2019 .times(3)
2020 .respond_with(cycle![
2021 status_code(503).body("try-again"),
2022 status_code(503).body("try-again"),
2023 json_encoded(json!({
2024 "access_token": "sts-only-token",
2025 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
2026 "token_type": "Bearer",
2027 "expires_in": 3600,
2028 }))
2029 ]),
2030 );
2031
2032 let creds = ProgrammaticBuilder::new(provider)
2033 .with_audience("test-audience")
2034 .with_subject_token_type("test-token-type")
2035 .with_token_url(sts_server.url("/token").to_string())
2036 .with_retry_policy(get_mock_auth_retry_policy(3))
2037 .with_backoff_policy(get_mock_backoff_policy())
2038 .with_retry_throttler(get_mock_retry_throttler())
2039 .build()
2040 .unwrap();
2041
2042 let headers = creds.headers(Extensions::new()).await.unwrap();
2043 match headers {
2044 CacheableResource::New { data, .. } => {
2045 let token = data.get("authorization").unwrap().to_str().unwrap();
2046 assert_eq!(token, "Bearer sts-only-token");
2047 }
2048 CacheableResource::NotModified => panic!("Expected new headers"),
2049 }
2050 sts_server.verify_and_clear();
2051 }
2052}