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