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 com.google.api.client.json.GenericJson; 35 import com.google.auth.http.HttpTransportFactory; 36 import com.google.common.annotations.VisibleForTesting; 37 import com.google.errorprone.annotations.CanIgnoreReturnValue; 38 import java.io.IOException; 39 import java.io.UnsupportedEncodingException; 40 import java.net.URLEncoder; 41 import java.util.ArrayList; 42 import java.util.Collection; 43 import java.util.HashMap; 44 import java.util.List; 45 import java.util.Map; 46 import javax.annotation.Nullable; 47 48 /** 49 * Credentials representing an AWS third-party identity for calling Google APIs. AWS security 50 * credentials are either sourced by calling EC2 metadata endpoints, environment variables, or a 51 * user provided supplier method. 52 * 53 * <p>By default, attempts to exchange the external credential for a GCP access token. 54 */ 55 public class AwsCredentials extends ExternalAccountCredentials { 56 57 static final String DEFAULT_REGIONAL_CREDENTIAL_VERIFICATION_URL = 58 "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15"; 59 60 static final String AWS_METRICS_HEADER_VALUE = "aws"; 61 62 private static final long serialVersionUID = -3670131891574618105L; 63 64 private final AwsSecurityCredentialsSupplier awsSecurityCredentialsSupplier; 65 private final ExternalAccountSupplierContext supplierContext; 66 // Regional credential verification url override. This needs to be its own value so we can 67 // correctly pass it to a builder. 68 @Nullable private final String regionalCredentialVerificationUrlOverride; 69 @Nullable private final String regionalCredentialVerificationUrl; 70 private final String metricsHeaderValue; 71 72 /** Internal constructor. See {@link AwsCredentials.Builder}. */ AwsCredentials(Builder builder)73 AwsCredentials(Builder builder) { 74 super(builder); 75 this.supplierContext = 76 ExternalAccountSupplierContext.newBuilder() 77 .setAudience(this.getAudience()) 78 .setSubjectTokenType(this.getSubjectTokenType()) 79 .build(); 80 81 // Check that one and only one of supplier or credential source are provided. 82 if (builder.awsSecurityCredentialsSupplier != null && builder.credentialSource != null) { 83 throw new IllegalArgumentException( 84 "AwsCredentials cannot have both an awsSecurityCredentialsSupplier and a credentialSource."); 85 } 86 if (builder.awsSecurityCredentialsSupplier == null && builder.credentialSource == null) { 87 throw new IllegalArgumentException( 88 "An awsSecurityCredentialsSupplier or a credentialSource must be provided."); 89 } 90 91 AwsCredentialSource credentialSource = (AwsCredentialSource) builder.credentialSource; 92 // Set regional credential verification url override if provided. 93 this.regionalCredentialVerificationUrlOverride = 94 builder.regionalCredentialVerificationUrlOverride; 95 96 // Set regional credential verification url depending on inputs. 97 if (this.regionalCredentialVerificationUrlOverride != null) { 98 this.regionalCredentialVerificationUrl = this.regionalCredentialVerificationUrlOverride; 99 } else if (credentialSource != null) { 100 this.regionalCredentialVerificationUrl = credentialSource.regionalCredentialVerificationUrl; 101 } else { 102 this.regionalCredentialVerificationUrl = DEFAULT_REGIONAL_CREDENTIAL_VERIFICATION_URL; 103 } 104 105 // If user has provided a security credential supplier, use that to retrieve the AWS security 106 // credentials. 107 if (builder.awsSecurityCredentialsSupplier != null) { 108 this.awsSecurityCredentialsSupplier = builder.awsSecurityCredentialsSupplier; 109 this.metricsHeaderValue = PROGRAMMATIC_METRICS_HEADER_VALUE; 110 } else { 111 this.awsSecurityCredentialsSupplier = 112 new InternalAwsSecurityCredentialsSupplier( 113 credentialSource, this.getEnvironmentProvider(), this.transportFactory); 114 this.metricsHeaderValue = AWS_METRICS_HEADER_VALUE; 115 } 116 } 117 118 @Override refreshAccessToken()119 public AccessToken refreshAccessToken() throws IOException { 120 StsTokenExchangeRequest.Builder stsTokenExchangeRequest = 121 StsTokenExchangeRequest.newBuilder(retrieveSubjectToken(), getSubjectTokenType()) 122 .setAudience(getAudience()); 123 124 // Add scopes, if possible. 125 Collection<String> scopes = getScopes(); 126 if (scopes != null && !scopes.isEmpty()) { 127 stsTokenExchangeRequest.setScopes(new ArrayList<>(scopes)); 128 } 129 130 return exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest.build()); 131 } 132 133 @Override retrieveSubjectToken()134 public String retrieveSubjectToken() throws IOException { 135 136 // The targeted region is required to generate the signed request. The regional 137 // endpoint must also be used. 138 String region = awsSecurityCredentialsSupplier.getRegion(supplierContext); 139 140 AwsSecurityCredentials credentials = 141 awsSecurityCredentialsSupplier.getCredentials(supplierContext); 142 143 // Generate the signed request to the AWS STS GetCallerIdentity API. 144 Map<String, String> headers = new HashMap<>(); 145 headers.put("x-goog-cloud-target-resource", getAudience()); 146 147 AwsRequestSigner signer = 148 AwsRequestSigner.newBuilder( 149 credentials, 150 "POST", 151 this.regionalCredentialVerificationUrl.replace("{region}", region), 152 region) 153 .setAdditionalHeaders(headers) 154 .build(); 155 156 AwsRequestSignature awsRequestSignature = signer.sign(); 157 return buildSubjectToken(awsRequestSignature); 158 } 159 160 /** Clones the AwsCredentials with the specified scopes. */ 161 @Override createScoped(Collection<String> newScopes)162 public GoogleCredentials createScoped(Collection<String> newScopes) { 163 return new AwsCredentials((AwsCredentials.Builder) newBuilder(this).setScopes(newScopes)); 164 } 165 166 @Override getCredentialSourceType()167 String getCredentialSourceType() { 168 return this.metricsHeaderValue; 169 } 170 buildSubjectToken(AwsRequestSignature signature)171 private String buildSubjectToken(AwsRequestSignature signature) 172 throws UnsupportedEncodingException { 173 Map<String, String> canonicalHeaders = signature.getCanonicalHeaders(); 174 List<GenericJson> headerList = new ArrayList<>(); 175 for (String headerName : canonicalHeaders.keySet()) { 176 headerList.add(formatTokenHeaderForSts(headerName, canonicalHeaders.get(headerName))); 177 } 178 179 headerList.add(formatTokenHeaderForSts("Authorization", signature.getAuthorizationHeader())); 180 181 // The canonical resource name of the workload identity pool provider. 182 headerList.add(formatTokenHeaderForSts("x-goog-cloud-target-resource", getAudience())); 183 184 GenericJson token = new GenericJson(); 185 token.setFactory(OAuth2Utils.JSON_FACTORY); 186 187 token.put("headers", headerList); 188 token.put("method", signature.getHttpMethod()); 189 token.put( 190 "url", this.regionalCredentialVerificationUrl.replace("{region}", signature.getRegion())); 191 return URLEncoder.encode(token.toString(), "UTF-8"); 192 } 193 194 @VisibleForTesting getRegionalCredentialVerificationUrl()195 String getRegionalCredentialVerificationUrl() { 196 return this.regionalCredentialVerificationUrl; 197 } 198 199 @VisibleForTesting getEnv(String name)200 String getEnv(String name) { 201 return System.getenv(name); 202 } 203 204 @VisibleForTesting getAwsSecurityCredentialsSupplier()205 AwsSecurityCredentialsSupplier getAwsSecurityCredentialsSupplier() { 206 return this.awsSecurityCredentialsSupplier; 207 } 208 209 @Nullable getRegionalCredentialVerificationUrlOverride()210 public String getRegionalCredentialVerificationUrlOverride() { 211 return this.regionalCredentialVerificationUrlOverride; 212 } 213 formatTokenHeaderForSts(String key, String value)214 private static GenericJson formatTokenHeaderForSts(String key, String value) { 215 // The GCP STS endpoint expects the headers to be formatted as: 216 // [ 217 // {key: 'x-amz-date', value: '...'}, 218 // {key: 'Authorization', value: '...'}, 219 // ... 220 // ] 221 GenericJson header = new GenericJson(); 222 header.setFactory(OAuth2Utils.JSON_FACTORY); 223 header.put("key", key); 224 header.put("value", value); 225 return header; 226 } 227 newBuilder()228 public static AwsCredentials.Builder newBuilder() { 229 return new AwsCredentials.Builder(); 230 } 231 newBuilder(AwsCredentials awsCredentials)232 public static AwsCredentials.Builder newBuilder(AwsCredentials awsCredentials) { 233 return new AwsCredentials.Builder(awsCredentials); 234 } 235 236 public static class Builder extends ExternalAccountCredentials.Builder { 237 238 private AwsSecurityCredentialsSupplier awsSecurityCredentialsSupplier; 239 240 private String regionalCredentialVerificationUrlOverride; 241 Builder()242 Builder() {} 243 Builder(AwsCredentials credentials)244 Builder(AwsCredentials credentials) { 245 super(credentials); 246 if (this.credentialSource == null) { 247 this.awsSecurityCredentialsSupplier = credentials.awsSecurityCredentialsSupplier; 248 } 249 this.regionalCredentialVerificationUrlOverride = 250 credentials.regionalCredentialVerificationUrlOverride; 251 } 252 253 /** 254 * Sets the AWS security credentials supplier. The supplier should return a valid {@code 255 * AwsSecurityCredentials} object and a valid AWS region. 256 * 257 * @param awsSecurityCredentialsSupplier the supplier to use. 258 * @return this {@code Builder} object 259 */ 260 @CanIgnoreReturnValue setAwsSecurityCredentialsSupplier( AwsSecurityCredentialsSupplier awsSecurityCredentialsSupplier)261 public Builder setAwsSecurityCredentialsSupplier( 262 AwsSecurityCredentialsSupplier awsSecurityCredentialsSupplier) { 263 this.awsSecurityCredentialsSupplier = awsSecurityCredentialsSupplier; 264 return this; 265 } 266 267 /** 268 * Sets the AWS regional credential verification URL. If set, will override any credential 269 * verification URL provided in the credential source. If not set, the credential verification 270 * URL will default to 271 * https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15" 272 * 273 * @param regionalCredentialVerificationUrlOverride the AWS credential verification url to set. 274 * @return this {@code Builder} object 275 */ 276 @CanIgnoreReturnValue setRegionalCredentialVerificationUrlOverride( String regionalCredentialVerificationUrlOverride)277 public Builder setRegionalCredentialVerificationUrlOverride( 278 String regionalCredentialVerificationUrlOverride) { 279 this.regionalCredentialVerificationUrlOverride = regionalCredentialVerificationUrlOverride; 280 return this; 281 } 282 283 @CanIgnoreReturnValue setHttpTransportFactory(HttpTransportFactory transportFactory)284 public Builder setHttpTransportFactory(HttpTransportFactory transportFactory) { 285 super.setHttpTransportFactory(transportFactory); 286 return this; 287 } 288 289 @CanIgnoreReturnValue setAudience(String audience)290 public Builder setAudience(String audience) { 291 super.setAudience(audience); 292 return this; 293 } 294 295 @CanIgnoreReturnValue setSubjectTokenType(String subjectTokenType)296 public Builder setSubjectTokenType(String subjectTokenType) { 297 super.setSubjectTokenType(subjectTokenType); 298 return this; 299 } 300 301 @CanIgnoreReturnValue setSubjectTokenType(SubjectTokenTypes subjectTokenType)302 public Builder setSubjectTokenType(SubjectTokenTypes subjectTokenType) { 303 super.setSubjectTokenType(subjectTokenType); 304 return this; 305 } 306 307 @CanIgnoreReturnValue setTokenUrl(String tokenUrl)308 public Builder setTokenUrl(String tokenUrl) { 309 super.setTokenUrl(tokenUrl); 310 return this; 311 } 312 313 @CanIgnoreReturnValue setCredentialSource(AwsCredentialSource credentialSource)314 public Builder setCredentialSource(AwsCredentialSource credentialSource) { 315 super.setCredentialSource(credentialSource); 316 return this; 317 } 318 319 @CanIgnoreReturnValue setServiceAccountImpersonationUrl(String serviceAccountImpersonationUrl)320 public Builder setServiceAccountImpersonationUrl(String serviceAccountImpersonationUrl) { 321 super.setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl); 322 return this; 323 } 324 325 @CanIgnoreReturnValue setTokenInfoUrl(String tokenInfoUrl)326 public Builder setTokenInfoUrl(String tokenInfoUrl) { 327 super.setTokenInfoUrl(tokenInfoUrl); 328 return this; 329 } 330 331 @CanIgnoreReturnValue setQuotaProjectId(String quotaProjectId)332 public Builder setQuotaProjectId(String quotaProjectId) { 333 super.setQuotaProjectId(quotaProjectId); 334 return this; 335 } 336 337 @CanIgnoreReturnValue setClientId(String clientId)338 public Builder setClientId(String clientId) { 339 super.setClientId(clientId); 340 return this; 341 } 342 343 @CanIgnoreReturnValue setClientSecret(String clientSecret)344 public Builder setClientSecret(String clientSecret) { 345 super.setClientSecret(clientSecret); 346 return this; 347 } 348 349 @CanIgnoreReturnValue setScopes(Collection<String> scopes)350 public Builder setScopes(Collection<String> scopes) { 351 super.setScopes(scopes); 352 return this; 353 } 354 355 @CanIgnoreReturnValue setWorkforcePoolUserProject(String workforcePoolUserProject)356 public Builder setWorkforcePoolUserProject(String workforcePoolUserProject) { 357 super.setWorkforcePoolUserProject(workforcePoolUserProject); 358 return this; 359 } 360 361 @CanIgnoreReturnValue setServiceAccountImpersonationOptions(Map<String, Object> optionsMap)362 public Builder setServiceAccountImpersonationOptions(Map<String, Object> optionsMap) { 363 super.setServiceAccountImpersonationOptions(optionsMap); 364 return this; 365 } 366 367 @CanIgnoreReturnValue setUniverseDomain(String universeDomain)368 public Builder setUniverseDomain(String universeDomain) { 369 super.setUniverseDomain(universeDomain); 370 return this; 371 } 372 373 @CanIgnoreReturnValue setEnvironmentProvider(EnvironmentProvider environmentProvider)374 Builder setEnvironmentProvider(EnvironmentProvider environmentProvider) { 375 super.setEnvironmentProvider(environmentProvider); 376 return this; 377 } 378 379 @Override build()380 public AwsCredentials build() { 381 return new AwsCredentials(this); 382 } 383 } 384 } 385