• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2021 Google LLC
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  *    * Redistributions of source code must retain the above copyright
9  * notice, this list of conditions and the following disclaimer.
10  *    * Redistributions in binary form must reproduce the above
11  * copyright notice, this list of conditions and the following disclaimer
12  * in the documentation and/or other materials provided with the
13  * distribution.
14  *
15  *    * Neither the name of Google LLC nor the names of its
16  * contributors may be used to endorse or promote products derived from
17  * this software without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30  */
31 
32 package com.google.auth.oauth2;
33 
34 import static com.google.common.base.Preconditions.checkNotNull;
35 
36 import com.google.api.client.http.HttpHeaders;
37 import com.google.api.client.json.GenericJson;
38 import com.google.api.client.json.JsonObjectParser;
39 import com.google.auth.RequestMetadataCallback;
40 import com.google.auth.http.HttpTransportFactory;
41 import com.google.common.base.MoreObjects;
42 import com.google.errorprone.annotations.CanIgnoreReturnValue;
43 import java.io.IOException;
44 import java.io.InputStream;
45 import java.math.BigDecimal;
46 import java.net.URI;
47 import java.nio.charset.StandardCharsets;
48 import java.util.ArrayList;
49 import java.util.Arrays;
50 import java.util.Collection;
51 import java.util.HashMap;
52 import java.util.List;
53 import java.util.Locale;
54 import java.util.Map;
55 import java.util.concurrent.Executor;
56 import java.util.regex.Pattern;
57 import javax.annotation.Nullable;
58 
59 /**
60  * Base external account credentials class.
61  *
62  * <p>Handles initializing external credentials, calls to the Security Token Service, and service
63  * account impersonation.
64  */
65 public abstract class ExternalAccountCredentials extends GoogleCredentials {
66 
67   private static final long serialVersionUID = 8049126194174465023L;
68 
69   private static final String CLOUD_PLATFORM_SCOPE =
70       "https://www.googleapis.com/auth/cloud-platform";
71 
72   static final String EXTERNAL_ACCOUNT_FILE_TYPE = "external_account";
73   static final String EXECUTABLE_SOURCE_KEY = "executable";
74 
75   static final String DEFAULT_TOKEN_URL = "https://sts.googleapis.com/v1/token";
76   static final String PROGRAMMATIC_METRICS_HEADER_VALUE = "programmatic";
77 
78   private final String transportFactoryClassName;
79   private final String audience;
80   private final String subjectTokenType;
81   private final String tokenUrl;
82   private final CredentialSource credentialSource;
83   private final Collection<String> scopes;
84   private final ServiceAccountImpersonationOptions serviceAccountImpersonationOptions;
85   private ExternalAccountMetricsHandler metricsHandler;
86 
87   @Nullable private final String tokenInfoUrl;
88   @Nullable private final String serviceAccountImpersonationUrl;
89   @Nullable private final String clientId;
90   @Nullable private final String clientSecret;
91 
92   // This is used for Workforce Pools. It is passed to the Security Token Service during token
93   // exchange in the `options` param and will be embedded in the token by the Security Token
94   // Service.
95   @Nullable private final String workforcePoolUserProject;
96 
97   protected transient HttpTransportFactory transportFactory;
98 
99   @Nullable protected ImpersonatedCredentials impersonatedCredentials;
100 
101   private EnvironmentProvider environmentProvider;
102 
103   /**
104    * Constructor with minimum identifying information and custom HTTP transport. Does not support
105    * workforce credentials.
106    *
107    * @param transportFactory HTTP transport factory, creates the transport used to get access tokens
108    * @param audience the Security Token Service audience, which is usually the fully specified
109    *     resource name of the workload/workforce pool provider
110    * @param subjectTokenType the Security Token Service subject token type based on the OAuth 2.0
111    *     token exchange spec. Indicates the type of the security token in the credential file
112    * @param tokenUrl the Security Token Service token exchange endpoint
113    * @param tokenInfoUrl the endpoint used to retrieve account related information. Required for
114    *     gCloud session account identification.
115    * @param credentialSource the external credential source
116    * @param serviceAccountImpersonationUrl the URL for the service account impersonation request.
117    *     This URL is required for some APIs. If this URL is not available, the access token from the
118    *     Security Token Service is used directly. May be null.
119    * @param quotaProjectId the project used for quota and billing purposes. May be null.
120    * @param clientId client ID of the service account from the console. May be null.
121    * @param clientSecret client secret of the service account from the console. May be null.
122    * @param scopes the scopes to request during the authorization grant. May be null.
123    */
ExternalAccountCredentials( HttpTransportFactory transportFactory, String audience, String subjectTokenType, String tokenUrl, CredentialSource credentialSource, @Nullable String tokenInfoUrl, @Nullable String serviceAccountImpersonationUrl, @Nullable String quotaProjectId, @Nullable String clientId, @Nullable String clientSecret, @Nullable Collection<String> scopes)124   protected ExternalAccountCredentials(
125       HttpTransportFactory transportFactory,
126       String audience,
127       String subjectTokenType,
128       String tokenUrl,
129       CredentialSource credentialSource,
130       @Nullable String tokenInfoUrl,
131       @Nullable String serviceAccountImpersonationUrl,
132       @Nullable String quotaProjectId,
133       @Nullable String clientId,
134       @Nullable String clientSecret,
135       @Nullable Collection<String> scopes) {
136     this(
137         transportFactory,
138         audience,
139         subjectTokenType,
140         tokenUrl,
141         credentialSource,
142         tokenInfoUrl,
143         serviceAccountImpersonationUrl,
144         quotaProjectId,
145         clientId,
146         clientSecret,
147         scopes,
148         /* environmentProvider= */ null);
149   }
150 
151   /**
152    * Constructor with minimum identifying information and custom HTTP transport. Does not support
153    * workforce credentials.
154    *
155    * @param transportFactory HTTP transport factory, creates the transport used to get access tokens
156    * @param audience the Security Token Service audience, which is usually the fully specified
157    *     resource name of the workload/workforce pool provider
158    * @param subjectTokenType the Security Token Service subject token type based on the OAuth 2.0
159    *     token exchange spec. Indicates the type of the security token in the credential file
160    * @param tokenUrl the Security Token Service token exchange endpoint
161    * @param tokenInfoUrl the endpoint used to retrieve account related information. Required for
162    *     gCloud session account identification.
163    * @param credentialSource the external credential source
164    * @param serviceAccountImpersonationUrl the URL for the service account impersonation request.
165    *     This URL is required for some APIs. If this URL is not available, the access token from the
166    *     Security Token Service is used directly. May be null.
167    * @param quotaProjectId the project used for quota and billing purposes. May be null.
168    * @param clientId client ID of the service account from the console. May be null.
169    * @param clientSecret client secret of the service account from the console. May be null.
170    * @param scopes the scopes to request during the authorization grant. May be null.
171    * @param environmentProvider the environment provider. May be null. Defaults to {@link
172    *     SystemEnvironmentProvider}.
173    */
ExternalAccountCredentials( HttpTransportFactory transportFactory, String audience, String subjectTokenType, String tokenUrl, CredentialSource credentialSource, @Nullable String tokenInfoUrl, @Nullable String serviceAccountImpersonationUrl, @Nullable String quotaProjectId, @Nullable String clientId, @Nullable String clientSecret, @Nullable Collection<String> scopes, @Nullable EnvironmentProvider environmentProvider)174   protected ExternalAccountCredentials(
175       HttpTransportFactory transportFactory,
176       String audience,
177       String subjectTokenType,
178       String tokenUrl,
179       CredentialSource credentialSource,
180       @Nullable String tokenInfoUrl,
181       @Nullable String serviceAccountImpersonationUrl,
182       @Nullable String quotaProjectId,
183       @Nullable String clientId,
184       @Nullable String clientSecret,
185       @Nullable Collection<String> scopes,
186       @Nullable EnvironmentProvider environmentProvider) {
187     super(/* accessToken= */ null, quotaProjectId);
188     this.transportFactory =
189         MoreObjects.firstNonNull(
190             transportFactory,
191             getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
192     this.transportFactoryClassName = checkNotNull(this.transportFactory.getClass().getName());
193     this.audience = checkNotNull(audience);
194     this.subjectTokenType = checkNotNull(subjectTokenType);
195     this.tokenUrl = checkNotNull(tokenUrl);
196     this.credentialSource = checkNotNull(credentialSource);
197     this.tokenInfoUrl = tokenInfoUrl;
198     this.serviceAccountImpersonationUrl = serviceAccountImpersonationUrl;
199     this.clientId = clientId;
200     this.clientSecret = clientSecret;
201     this.scopes =
202         (scopes == null || scopes.isEmpty()) ? Arrays.asList(CLOUD_PLATFORM_SCOPE) : scopes;
203     this.environmentProvider =
204         environmentProvider == null ? SystemEnvironmentProvider.getInstance() : environmentProvider;
205     this.workforcePoolUserProject = null;
206     this.serviceAccountImpersonationOptions =
207         new ServiceAccountImpersonationOptions(new HashMap<String, Object>());
208 
209     validateTokenUrl(tokenUrl);
210     if (serviceAccountImpersonationUrl != null) {
211       validateServiceAccountImpersonationInfoUrl(serviceAccountImpersonationUrl);
212     }
213 
214     this.metricsHandler = new ExternalAccountMetricsHandler(this);
215   }
216 
217   /**
218    * Internal constructor with minimum identifying information and custom HTTP transport. See {@link
219    * ExternalAccountCredentials.Builder}.
220    *
221    * @param builder the {@code Builder} object used to construct the credentials.
222    */
ExternalAccountCredentials(ExternalAccountCredentials.Builder builder)223   protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder) {
224     super(builder);
225     this.transportFactory =
226         MoreObjects.firstNonNull(
227             builder.transportFactory,
228             getFromServiceLoader(HttpTransportFactory.class, OAuth2Utils.HTTP_TRANSPORT_FACTORY));
229     this.transportFactoryClassName = checkNotNull(this.transportFactory.getClass().getName());
230     this.audience = checkNotNull(builder.audience);
231     this.subjectTokenType = checkNotNull(builder.subjectTokenType);
232     this.credentialSource = builder.credentialSource;
233     this.tokenInfoUrl = builder.tokenInfoUrl;
234     this.serviceAccountImpersonationUrl = builder.serviceAccountImpersonationUrl;
235     this.clientId = builder.clientId;
236     this.clientSecret = builder.clientSecret;
237     this.tokenUrl = builder.tokenUrl == null ? DEFAULT_TOKEN_URL : builder.tokenUrl;
238     this.scopes =
239         (builder.scopes == null || builder.scopes.isEmpty())
240             ? Arrays.asList(CLOUD_PLATFORM_SCOPE)
241             : builder.scopes;
242     this.environmentProvider =
243         builder.environmentProvider == null
244             ? SystemEnvironmentProvider.getInstance()
245             : builder.environmentProvider;
246     this.serviceAccountImpersonationOptions =
247         builder.serviceAccountImpersonationOptions == null
248             ? new ServiceAccountImpersonationOptions(new HashMap<String, Object>())
249             : builder.serviceAccountImpersonationOptions;
250 
251     this.workforcePoolUserProject = builder.workforcePoolUserProject;
252     if (workforcePoolUserProject != null && !isWorkforcePoolConfiguration()) {
253       throw new IllegalArgumentException(
254           "The workforce_pool_user_project parameter should only be provided for a Workforce Pool configuration.");
255     }
256 
257     validateTokenUrl(tokenUrl);
258     if (serviceAccountImpersonationUrl != null) {
259       validateServiceAccountImpersonationInfoUrl(serviceAccountImpersonationUrl);
260     }
261 
262     this.metricsHandler =
263         builder.metricsHandler == null
264             ? new ExternalAccountMetricsHandler(this)
265             : builder.metricsHandler;
266   }
267 
buildImpersonatedCredentials()268   ImpersonatedCredentials buildImpersonatedCredentials() {
269     if (serviceAccountImpersonationUrl == null) {
270       return null;
271     }
272     // Create a copy of this instance without service account impersonation.
273     ExternalAccountCredentials sourceCredentials;
274     if (this instanceof AwsCredentials) {
275       sourceCredentials =
276           AwsCredentials.newBuilder((AwsCredentials) this)
277               .setServiceAccountImpersonationUrl(null)
278               .build();
279     } else if (this instanceof PluggableAuthCredentials) {
280       sourceCredentials =
281           PluggableAuthCredentials.newBuilder((PluggableAuthCredentials) this)
282               .setServiceAccountImpersonationUrl(null)
283               .build();
284     } else {
285       sourceCredentials =
286           IdentityPoolCredentials.newBuilder((IdentityPoolCredentials) this)
287               .setServiceAccountImpersonationUrl(null)
288               .build();
289     }
290 
291     String targetPrincipal =
292         ImpersonatedCredentials.extractTargetPrincipal(serviceAccountImpersonationUrl);
293     return ImpersonatedCredentials.newBuilder()
294         .setSourceCredentials(sourceCredentials)
295         .setHttpTransportFactory(transportFactory)
296         .setTargetPrincipal(targetPrincipal)
297         .setScopes(new ArrayList<>(scopes))
298         .setLifetime(this.serviceAccountImpersonationOptions.lifetime)
299         .setIamEndpointOverride(serviceAccountImpersonationUrl)
300         .build();
301   }
302 
303   @Override
getRequestMetadata( URI uri, Executor executor, final RequestMetadataCallback callback)304   public void getRequestMetadata(
305       URI uri, Executor executor, final RequestMetadataCallback callback) {
306     super.getRequestMetadata(
307         uri,
308         executor,
309         new RequestMetadataCallback() {
310           @Override
311           public void onSuccess(Map<String, List<String>> metadata) {
312             metadata = addQuotaProjectIdToRequestMetadata(quotaProjectId, metadata);
313             callback.onSuccess(metadata);
314           }
315 
316           @Override
317           public void onFailure(Throwable exception) {
318             callback.onFailure(exception);
319           }
320         });
321   }
322 
323   @Override
getRequestMetadata(URI uri)324   public Map<String, List<String>> getRequestMetadata(URI uri) throws IOException {
325     Map<String, List<String>> requestMetadata = super.getRequestMetadata(uri);
326     return addQuotaProjectIdToRequestMetadata(quotaProjectId, requestMetadata);
327   }
328 
329   /**
330    * Returns credentials defined by a JSON file stream.
331    *
332    * <p>Returns {@link IdentityPoolCredentials} or {@link AwsCredentials}.
333    *
334    * @param credentialsStream the stream with the credential definition
335    * @return the credential defined by the credentialsStream
336    * @throws IOException if the credential cannot be created from the stream
337    */
fromStream(InputStream credentialsStream)338   public static ExternalAccountCredentials fromStream(InputStream credentialsStream)
339       throws IOException {
340     return fromStream(credentialsStream, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
341   }
342 
343   /**
344    * Returns credentials defined by a JSON file stream.
345    *
346    * <p>Returns a {@link IdentityPoolCredentials} or {@link AwsCredentials}.
347    *
348    * @param credentialsStream the stream with the credential definition
349    * @param transportFactory the HTTP transport factory used to create the transport to get access
350    *     tokens
351    * @return the credential defined by the credentialsStream
352    * @throws IOException if the credential cannot be created from the stream
353    */
fromStream( InputStream credentialsStream, HttpTransportFactory transportFactory)354   public static ExternalAccountCredentials fromStream(
355       InputStream credentialsStream, HttpTransportFactory transportFactory) throws IOException {
356     checkNotNull(credentialsStream);
357     checkNotNull(transportFactory);
358 
359     JsonObjectParser parser = new JsonObjectParser(OAuth2Utils.JSON_FACTORY);
360     GenericJson fileContents =
361         parser.parseAndClose(credentialsStream, StandardCharsets.UTF_8, GenericJson.class);
362     try {
363       return fromJson(fileContents, transportFactory);
364     } catch (ClassCastException | IllegalArgumentException e) {
365       throw new CredentialFormatException("An invalid input stream was provided.", e);
366     }
367   }
368 
369   /**
370    * Returns external account credentials defined by JSON using the format generated by gCloud.
371    *
372    * @param json a map from the JSON representing the credentials
373    * @param transportFactory HTTP transport factory, creates the transport used to get access tokens
374    * @return the credentials defined by the JSON
375    */
376   @SuppressWarnings("unchecked")
fromJson( Map<String, Object> json, HttpTransportFactory transportFactory)377   static ExternalAccountCredentials fromJson(
378       Map<String, Object> json, HttpTransportFactory transportFactory) {
379     checkNotNull(json);
380     checkNotNull(transportFactory);
381 
382     String audience = (String) json.get("audience");
383     String subjectTokenType = (String) json.get("subject_token_type");
384     String tokenUrl = (String) json.get("token_url");
385 
386     Map<String, Object> credentialSourceMap = (Map<String, Object>) json.get("credential_source");
387 
388     // Optional params.
389     String serviceAccountImpersonationUrl = (String) json.get("service_account_impersonation_url");
390     String tokenInfoUrl = (String) json.get("token_info_url");
391     String clientId = (String) json.get("client_id");
392     String clientSecret = (String) json.get("client_secret");
393     String quotaProjectId = (String) json.get("quota_project_id");
394     String userProject = (String) json.get("workforce_pool_user_project");
395     String universeDomain = (String) json.get("universe_domain");
396     Map<String, Object> impersonationOptionsMap =
397         (Map<String, Object>) json.get("service_account_impersonation");
398 
399     if (impersonationOptionsMap == null) {
400       impersonationOptionsMap = new HashMap<String, Object>();
401     }
402 
403     if (isAwsCredential(credentialSourceMap)) {
404       return AwsCredentials.newBuilder()
405           .setHttpTransportFactory(transportFactory)
406           .setAudience(audience)
407           .setSubjectTokenType(subjectTokenType)
408           .setTokenUrl(tokenUrl)
409           .setTokenInfoUrl(tokenInfoUrl)
410           .setCredentialSource(new AwsCredentialSource(credentialSourceMap))
411           .setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl)
412           .setQuotaProjectId(quotaProjectId)
413           .setClientId(clientId)
414           .setClientSecret(clientSecret)
415           .setServiceAccountImpersonationOptions(impersonationOptionsMap)
416           .setUniverseDomain(universeDomain)
417           .build();
418     } else if (isPluggableAuthCredential(credentialSourceMap)) {
419       return PluggableAuthCredentials.newBuilder()
420           .setHttpTransportFactory(transportFactory)
421           .setAudience(audience)
422           .setSubjectTokenType(subjectTokenType)
423           .setTokenUrl(tokenUrl)
424           .setTokenInfoUrl(tokenInfoUrl)
425           .setCredentialSource(new PluggableAuthCredentialSource(credentialSourceMap))
426           .setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl)
427           .setQuotaProjectId(quotaProjectId)
428           .setClientId(clientId)
429           .setClientSecret(clientSecret)
430           .setWorkforcePoolUserProject(userProject)
431           .setServiceAccountImpersonationOptions(impersonationOptionsMap)
432           .setUniverseDomain(universeDomain)
433           .build();
434     }
435     return IdentityPoolCredentials.newBuilder()
436         .setHttpTransportFactory(transportFactory)
437         .setAudience(audience)
438         .setSubjectTokenType(subjectTokenType)
439         .setTokenUrl(tokenUrl)
440         .setTokenInfoUrl(tokenInfoUrl)
441         .setCredentialSource(new IdentityPoolCredentialSource(credentialSourceMap))
442         .setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl)
443         .setQuotaProjectId(quotaProjectId)
444         .setClientId(clientId)
445         .setClientSecret(clientSecret)
446         .setWorkforcePoolUserProject(userProject)
447         .setServiceAccountImpersonationOptions(impersonationOptionsMap)
448         .setUniverseDomain(universeDomain)
449         .build();
450   }
451 
isPluggableAuthCredential(Map<String, Object> credentialSource)452   private static boolean isPluggableAuthCredential(Map<String, Object> credentialSource) {
453     // Pluggable Auth is enabled via a nested executable field in the credential source.
454     return credentialSource.containsKey(EXECUTABLE_SOURCE_KEY);
455   }
456 
isAwsCredential(Map<String, Object> credentialSource)457   private static boolean isAwsCredential(Map<String, Object> credentialSource) {
458     return credentialSource.containsKey("environment_id")
459         && ((String) credentialSource.get("environment_id")).startsWith("aws");
460   }
461 
shouldBuildImpersonatedCredential()462   private boolean shouldBuildImpersonatedCredential() {
463     return this.serviceAccountImpersonationUrl != null && this.impersonatedCredentials == null;
464   }
465 
466   /**
467    * Exchanges the external credential for a Google Cloud access token.
468    *
469    * @param stsTokenExchangeRequest the Security Token Service token exchange request
470    * @return the access token returned by the Security Token Service
471    * @throws OAuthException if the call to the Security Token Service fails
472    */
exchangeExternalCredentialForAccessToken( StsTokenExchangeRequest stsTokenExchangeRequest)473   protected AccessToken exchangeExternalCredentialForAccessToken(
474       StsTokenExchangeRequest stsTokenExchangeRequest) throws IOException {
475     // Handle service account impersonation if necessary.
476     if (this.shouldBuildImpersonatedCredential()) {
477       this.impersonatedCredentials = this.buildImpersonatedCredentials();
478     }
479     if (this.impersonatedCredentials != null) {
480       return this.impersonatedCredentials.refreshAccessToken();
481     }
482 
483     StsRequestHandler.Builder requestHandler =
484         StsRequestHandler.newBuilder(
485             tokenUrl, stsTokenExchangeRequest, transportFactory.create().createRequestFactory());
486 
487     // If this credential was initialized with a Workforce configuration then the
488     // workforcePoolUserProject must be passed to the Security Token Service via the internal
489     // options param.
490     if (isWorkforcePoolConfiguration()) {
491       GenericJson options = new GenericJson();
492       options.setFactory(OAuth2Utils.JSON_FACTORY);
493       options.put("userProject", workforcePoolUserProject);
494       requestHandler.setInternalOptions(options.toString());
495     }
496 
497     // Set BYOID Metrics header.
498     HttpHeaders additionalHeaders = new HttpHeaders();
499     additionalHeaders.set(
500         MetricsUtils.API_CLIENT_HEADER, this.metricsHandler.getExternalAccountMetricsHeader());
501     requestHandler.setHeaders(additionalHeaders);
502 
503     if (stsTokenExchangeRequest.getInternalOptions() != null) {
504       // Overwrite internal options. Let subclass handle setting options.
505       requestHandler.setInternalOptions(stsTokenExchangeRequest.getInternalOptions());
506     }
507 
508     StsTokenExchangeResponse response = requestHandler.build().exchangeToken();
509     return response.getAccessToken();
510   }
511 
512   /**
513    * Retrieves the external subject token to be exchanged for a Google Cloud access token.
514    *
515    * <p>Must be implemented by subclasses as the retrieval method is dependent on the credential
516    * source.
517    *
518    * @return the external subject token
519    * @throws IOException if the subject token cannot be retrieved
520    */
retrieveSubjectToken()521   public abstract String retrieveSubjectToken() throws IOException;
522 
getAudience()523   public String getAudience() {
524     return audience;
525   }
526 
getSubjectTokenType()527   public String getSubjectTokenType() {
528     return subjectTokenType;
529   }
530 
getTokenUrl()531   public String getTokenUrl() {
532     return tokenUrl;
533   }
534 
getTokenInfoUrl()535   public String getTokenInfoUrl() {
536     return tokenInfoUrl;
537   }
538 
getCredentialSource()539   public CredentialSource getCredentialSource() {
540     return credentialSource;
541   }
542 
543   @Nullable
getServiceAccountImpersonationUrl()544   public String getServiceAccountImpersonationUrl() {
545     return serviceAccountImpersonationUrl;
546   }
547 
548   /** @return The service account email to be impersonated, if available */
549   @Nullable
getServiceAccountEmail()550   public String getServiceAccountEmail() {
551     if (serviceAccountImpersonationUrl == null || serviceAccountImpersonationUrl.isEmpty()) {
552       return null;
553     }
554     return ImpersonatedCredentials.extractTargetPrincipal(serviceAccountImpersonationUrl);
555   }
556 
557   @Nullable
getClientId()558   public String getClientId() {
559     return clientId;
560   }
561 
562   @Nullable
getClientSecret()563   public String getClientSecret() {
564     return clientSecret;
565   }
566 
567   @Nullable
getScopes()568   public Collection<String> getScopes() {
569     return scopes;
570   }
571 
572   @Nullable
getWorkforcePoolUserProject()573   public String getWorkforcePoolUserProject() {
574     return workforcePoolUserProject;
575   }
576 
577   @Nullable
getServiceAccountImpersonationOptions()578   public ServiceAccountImpersonationOptions getServiceAccountImpersonationOptions() {
579     return serviceAccountImpersonationOptions;
580   }
581 
getCredentialSourceType()582   String getCredentialSourceType() {
583     return "unknown";
584   }
585 
getEnvironmentProvider()586   EnvironmentProvider getEnvironmentProvider() {
587     return environmentProvider;
588   }
589 
590   /**
591    * @return whether the current configuration is for Workforce Pools (which enable 3p user
592    *     identities, rather than workloads)
593    */
isWorkforcePoolConfiguration()594   public boolean isWorkforcePoolConfiguration() {
595     Pattern workforceAudiencePattern =
596         Pattern.compile("^//iam.googleapis.com/locations/.+/workforcePools/.+/providers/.+$");
597     return workforcePoolUserProject != null
598         && workforceAudiencePattern.matcher(getAudience()).matches();
599   }
600 
validateTokenUrl(String tokenUrl)601   static void validateTokenUrl(String tokenUrl) {
602     if (!isValidUrl(tokenUrl)) {
603       throw new IllegalArgumentException("The provided token URL is invalid.");
604     }
605   }
606 
validateServiceAccountImpersonationInfoUrl(String serviceAccountImpersonationUrl)607   static void validateServiceAccountImpersonationInfoUrl(String serviceAccountImpersonationUrl) {
608     if (!isValidUrl(serviceAccountImpersonationUrl)) {
609       throw new IllegalArgumentException(
610           "The provided service account impersonation URL is invalid.");
611     }
612   }
613 
614   /** Returns true if the provided URL's scheme is valid and is HTTPS. */
isValidUrl(String url)615   private static boolean isValidUrl(String url) {
616     URI uri;
617 
618     try {
619       uri = URI.create(url);
620     } catch (Exception e) {
621       return false;
622     }
623 
624     // Scheme must be https and host must not be null.
625     if (uri.getScheme() == null
626         || uri.getHost() == null
627         || !"https".equals(uri.getScheme().toLowerCase(Locale.US))) {
628       return false;
629     }
630 
631     return true;
632   }
633 
634   /**
635    * Encapsulates the service account impersonation options portion of the configuration for
636    * ExternalAccountCredentials.
637    *
638    * <p>If token_lifetime_seconds is not specified, the library will default to a 1-hour lifetime.
639    *
640    * <pre>
641    * Sample configuration:
642    * {
643    *   ...
644    *   "service_account_impersonation": {
645    *     "token_lifetime_seconds": 2800
646    *    }
647    * }
648    * </pre>
649    */
650   static final class ServiceAccountImpersonationOptions implements java.io.Serializable {
651 
652     private static final long serialVersionUID = 4250771921886280953L;
653     private static final int DEFAULT_TOKEN_LIFETIME_SECONDS = 3600;
654     private static final int MAXIMUM_TOKEN_LIFETIME_SECONDS = 43200;
655     private static final int MINIMUM_TOKEN_LIFETIME_SECONDS = 600;
656     private static final String TOKEN_LIFETIME_SECONDS_KEY = "token_lifetime_seconds";
657 
658     private final int lifetime;
659 
660     final boolean customTokenLifetimeRequested;
661 
ServiceAccountImpersonationOptions(Map<String, Object> optionsMap)662     ServiceAccountImpersonationOptions(Map<String, Object> optionsMap) {
663       customTokenLifetimeRequested = optionsMap.containsKey(TOKEN_LIFETIME_SECONDS_KEY);
664       if (!customTokenLifetimeRequested) {
665         lifetime = DEFAULT_TOKEN_LIFETIME_SECONDS;
666         return;
667       }
668 
669       try {
670         Object lifetimeValue = optionsMap.get(TOKEN_LIFETIME_SECONDS_KEY);
671         if (lifetimeValue instanceof BigDecimal) {
672           lifetime = ((BigDecimal) lifetimeValue).intValue();
673         } else if (optionsMap.get(TOKEN_LIFETIME_SECONDS_KEY) instanceof Integer) {
674           lifetime = (int) lifetimeValue;
675         } else {
676           lifetime = Integer.parseInt((String) lifetimeValue);
677         }
678       } catch (NumberFormatException | ArithmeticException e) {
679         throw new IllegalArgumentException(
680             "Value of \"token_lifetime_seconds\" field could not be parsed into an integer.", e);
681       }
682 
683       if (lifetime < MINIMUM_TOKEN_LIFETIME_SECONDS || lifetime > MAXIMUM_TOKEN_LIFETIME_SECONDS) {
684         throw new IllegalArgumentException(
685             String.format(
686                 "The \"token_lifetime_seconds\" field must be between %s and %s seconds.",
687                 MINIMUM_TOKEN_LIFETIME_SECONDS, MAXIMUM_TOKEN_LIFETIME_SECONDS));
688       }
689     }
690 
getLifetime()691     int getLifetime() {
692       return lifetime;
693     }
694   }
695 
696   /** Base builder for external account credentials. */
697   public abstract static class Builder extends GoogleCredentials.Builder {
698 
699     protected String audience;
700     protected String subjectTokenType;
701     protected String tokenUrl;
702     protected String tokenInfoUrl;
703     protected CredentialSource credentialSource;
704     protected EnvironmentProvider environmentProvider;
705     protected HttpTransportFactory transportFactory;
706 
707     @Nullable protected String serviceAccountImpersonationUrl;
708     @Nullable protected String clientId;
709     @Nullable protected String clientSecret;
710     @Nullable protected Collection<String> scopes;
711     @Nullable protected String workforcePoolUserProject;
712     @Nullable protected ServiceAccountImpersonationOptions serviceAccountImpersonationOptions;
713 
714     /* The field is not being used and value not set. Superseded by the same field in the
715     {@link GoogleCredential.Builder}.
716     */
717     @Nullable @Deprecated protected String universeDomain;
718 
719     @Nullable protected ExternalAccountMetricsHandler metricsHandler;
720 
Builder()721     protected Builder() {}
722 
Builder(ExternalAccountCredentials credentials)723     protected Builder(ExternalAccountCredentials credentials) {
724       super(credentials);
725       this.transportFactory = credentials.transportFactory;
726       this.audience = credentials.audience;
727       this.subjectTokenType = credentials.subjectTokenType;
728       this.tokenUrl = credentials.tokenUrl;
729       this.tokenInfoUrl = credentials.tokenInfoUrl;
730       this.serviceAccountImpersonationUrl = credentials.serviceAccountImpersonationUrl;
731       this.credentialSource = credentials.credentialSource;
732       this.clientId = credentials.clientId;
733       this.clientSecret = credentials.clientSecret;
734       this.scopes = credentials.scopes;
735       this.environmentProvider = credentials.environmentProvider;
736       this.workforcePoolUserProject = credentials.workforcePoolUserProject;
737       this.serviceAccountImpersonationOptions = credentials.serviceAccountImpersonationOptions;
738       this.metricsHandler = credentials.metricsHandler;
739     }
740 
741     /**
742      * Sets the HTTP transport factory, creates the transport used to get access tokens.
743      *
744      * @param transportFactory the {@code HttpTransportFactory} to set
745      * @return this {@code Builder} object
746      */
747     @CanIgnoreReturnValue
setHttpTransportFactory(HttpTransportFactory transportFactory)748     public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) {
749       this.transportFactory = transportFactory;
750       return this;
751     }
752 
753     /**
754      * Sets the Security Token Service audience, which is usually the fully specified resource name
755      * of the workload/workforce pool provider.
756      *
757      * @param audience the Security Token Service audience to set
758      * @return this {@code Builder} object
759      */
760     @CanIgnoreReturnValue
setAudience(String audience)761     public Builder setAudience(String audience) {
762       this.audience = audience;
763       return this;
764     }
765 
766     /**
767      * Sets the Security Token Service subject token type based on the OAuth 2.0 token exchange
768      * spec. Indicates the type of the security token in the credential file.
769      *
770      * @param subjectTokenType the Security Token Service subject token type to set
771      * @return this {@code Builder} object
772      */
773     @CanIgnoreReturnValue
setSubjectTokenType(String subjectTokenType)774     public Builder setSubjectTokenType(String subjectTokenType) {
775       this.subjectTokenType = subjectTokenType;
776       return this;
777     }
778 
779     /**
780      * Sets the Security Token Service subject token type based on the OAuth 2.0 token exchange
781      * spec. Indicates the type of the security token in the credential file.
782      *
783      * @param subjectTokenType the {@code SubjectTokenType} to set
784      * @return this {@code Builder} object
785      */
786     @CanIgnoreReturnValue
setSubjectTokenType(SubjectTokenTypes subjectTokenType)787     public Builder setSubjectTokenType(SubjectTokenTypes subjectTokenType) {
788       this.subjectTokenType = subjectTokenType.value;
789       return this;
790     }
791 
792     /**
793      * Sets the Security Token Service token exchange endpoint.
794      *
795      * @param tokenUrl the Security Token Service token exchange url to set
796      * @return this {@code Builder} object
797      */
798     @CanIgnoreReturnValue
setTokenUrl(String tokenUrl)799     public Builder setTokenUrl(String tokenUrl) {
800       this.tokenUrl = tokenUrl;
801       return this;
802     }
803 
804     /**
805      * Sets the external credential source.
806      *
807      * @param credentialSource the {@code CredentialSource} to set
808      * @return this {@code Builder} object
809      */
810     @CanIgnoreReturnValue
setCredentialSource(CredentialSource credentialSource)811     public Builder setCredentialSource(CredentialSource credentialSource) {
812       this.credentialSource = credentialSource;
813       return this;
814     }
815 
816     /**
817      * Sets the optional URL used for service account impersonation, which is required for some
818      * APIs. If this URL is not available, the access token from the Security Token Service is used
819      * directly.
820      *
821      * @param serviceAccountImpersonationUrl the service account impersonation url to set
822      * @return this {@code Builder} object
823      */
824     @CanIgnoreReturnValue
setServiceAccountImpersonationUrl(String serviceAccountImpersonationUrl)825     public Builder setServiceAccountImpersonationUrl(String serviceAccountImpersonationUrl) {
826       this.serviceAccountImpersonationUrl = serviceAccountImpersonationUrl;
827       return this;
828     }
829 
830     /**
831      * Sets the optional endpoint used to retrieve account related information. Required for gCloud
832      * session account identification.
833      *
834      * @param tokenInfoUrl the token info url to set
835      * @return this {@code Builder} object
836      */
837     @CanIgnoreReturnValue
setTokenInfoUrl(String tokenInfoUrl)838     public Builder setTokenInfoUrl(String tokenInfoUrl) {
839       this.tokenInfoUrl = tokenInfoUrl;
840       return this;
841     }
842 
843     /**
844      * Sets the optional project used for quota and billing purposes.
845      *
846      * @param quotaProjectId the quota and billing project id to set
847      * @return this {@code Builder} object
848      */
849     @Override
850     @CanIgnoreReturnValue
setQuotaProjectId(String quotaProjectId)851     public Builder setQuotaProjectId(String quotaProjectId) {
852       super.setQuotaProjectId(quotaProjectId);
853       return this;
854     }
855 
856     /**
857      * Sets the optional client ID of the service account from the console.
858      *
859      * @param clientId the service account client id to set
860      * @return this {@code Builder} object
861      */
862     @CanIgnoreReturnValue
setClientId(String clientId)863     public Builder setClientId(String clientId) {
864       this.clientId = clientId;
865       return this;
866     }
867 
868     /**
869      * Sets the optional client secret of the service account from the console.
870      *
871      * @param clientSecret the service account client secret to set
872      * @return this {@code Builder} object
873      */
874     @CanIgnoreReturnValue
setClientSecret(String clientSecret)875     public Builder setClientSecret(String clientSecret) {
876       this.clientSecret = clientSecret;
877       return this;
878     }
879 
880     /**
881      * Sets the optional scopes to request during the authorization grant.
882      *
883      * @param scopes the request scopes to set
884      * @return this {@code Builder} object
885      */
886     @CanIgnoreReturnValue
setScopes(Collection<String> scopes)887     public Builder setScopes(Collection<String> scopes) {
888       this.scopes = scopes;
889       return this;
890     }
891 
892     /**
893      * Sets the optional workforce pool user project number when the credential corresponds to a
894      * workforce pool and not a workload identity pool. The underlying principal must still have
895      * serviceusage.services.use IAM permission to use the project for billing/quota.
896      *
897      * @param workforcePoolUserProject the workforce pool user project number to set
898      * @return this {@code Builder} object
899      */
900     @CanIgnoreReturnValue
setWorkforcePoolUserProject(String workforcePoolUserProject)901     public Builder setWorkforcePoolUserProject(String workforcePoolUserProject) {
902       this.workforcePoolUserProject = workforcePoolUserProject;
903       return this;
904     }
905 
906     /**
907      * Sets the optional service account impersonation options.
908      *
909      * @param optionsMap the service account impersonation options to set
910      * @return this {@code Builder} object
911      */
912     @CanIgnoreReturnValue
setServiceAccountImpersonationOptions(Map<String, Object> optionsMap)913     public Builder setServiceAccountImpersonationOptions(Map<String, Object> optionsMap) {
914       this.serviceAccountImpersonationOptions = new ServiceAccountImpersonationOptions(optionsMap);
915       return this;
916     }
917 
918     /**
919      * Sets the optional universe domain.
920      *
921      * @param universeDomain the universe domain to set
922      * @return this {@code Builder} object
923      */
924     @CanIgnoreReturnValue
925     @Override
setUniverseDomain(String universeDomain)926     public Builder setUniverseDomain(String universeDomain) {
927       super.setUniverseDomain(universeDomain);
928       return this;
929     }
930 
931     /**
932      * Sets the optional Environment Provider.
933      *
934      * @param environmentProvider the {@code EnvironmentProvider} to set
935      * @return this {@code Builder} object
936      */
937     @CanIgnoreReturnValue
setEnvironmentProvider(EnvironmentProvider environmentProvider)938     Builder setEnvironmentProvider(EnvironmentProvider environmentProvider) {
939       this.environmentProvider = environmentProvider;
940       return this;
941     }
942 
943     @Override
build()944     public abstract ExternalAccountCredentials build();
945   }
946 
947   /**
948    * Enum specifying values for the subjectTokenType field in {@code ExternalAccountCredentials}.
949    */
950   public enum SubjectTokenTypes {
951     AWS4("urn:ietf:params:aws:token-type:aws4_request"),
952     JWT("urn:ietf:params:oauth:token-type:jwt"),
953     SAML2("urn:ietf:params:oauth:token-type:saml2"),
954     ID_TOKEN("urn:ietf:params:oauth:token-type:id_token");
955 
956     public final String value;
957 
SubjectTokenTypes(String value)958     private SubjectTokenTypes(String value) {
959       this.value = value;
960     }
961   }
962 
963   /** Base credential source class. Dictates the retrieval method of the external credential. */
964   abstract static class CredentialSource implements java.io.Serializable {
965 
966     private static final long serialVersionUID = 8204657811562399944L;
967 
CredentialSource(Map<String, Object> credentialSourceMap)968     CredentialSource(Map<String, Object> credentialSourceMap) {
969       checkNotNull(credentialSourceMap);
970     }
971   }
972 }
973