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