1 /* 2 * Copyright 2024 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.http.GenericUrl; 35 import com.google.api.client.http.HttpContent; 36 import com.google.api.client.http.HttpHeaders; 37 import com.google.api.client.http.HttpMethods; 38 import com.google.api.client.http.HttpRequest; 39 import com.google.api.client.http.HttpRequestFactory; 40 import com.google.api.client.http.HttpResponse; 41 import com.google.api.client.json.GenericJson; 42 import com.google.api.client.json.JsonParser; 43 import com.google.auth.http.HttpTransportFactory; 44 import com.google.common.annotations.VisibleForTesting; 45 import com.google.common.collect.ImmutableList; 46 import java.io.IOException; 47 import java.util.HashMap; 48 import java.util.List; 49 import java.util.Map; 50 import javax.annotation.Nullable; 51 52 /** 53 * Internal provider for retrieving AWS security credentials for {@Link AwsCredentials} to exchange 54 * for GCP access tokens. The credentials are retrieved either via environment variables or metadata 55 * endpoints. 56 */ 57 class InternalAwsSecurityCredentialsSupplier implements AwsSecurityCredentialsSupplier { 58 private static final long serialVersionUID = 4438370785261365013L; 59 60 // Supported environment variables. 61 static final String AWS_REGION = "AWS_REGION"; 62 static final String AWS_DEFAULT_REGION = "AWS_DEFAULT_REGION"; 63 static final String AWS_ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID"; 64 static final String AWS_SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY"; 65 static final String AWS_SESSION_TOKEN = "AWS_SESSION_TOKEN"; 66 67 static final String AWS_IMDSV2_SESSION_TOKEN_HEADER = "x-aws-ec2-metadata-token"; 68 static final String AWS_IMDSV2_SESSION_TOKEN_TTL_HEADER = "x-aws-ec2-metadata-token-ttl-seconds"; 69 static final String AWS_IMDSV2_SESSION_TOKEN_TTL = "300"; 70 71 private final AwsCredentialSource awsCredentialSource; 72 private EnvironmentProvider environmentProvider; 73 private transient HttpTransportFactory transportFactory; 74 75 /** 76 * Constructor for InternalAwsSecurityCredentialsProvider 77 * 78 * @param awsCredentialSource the credential source to use. 79 * @param environmentProvider the environment provider to use for environment variables. 80 * @param transportFactory the transport factory to use for metadata requests. 81 */ InternalAwsSecurityCredentialsSupplier( AwsCredentialSource awsCredentialSource, EnvironmentProvider environmentProvider, HttpTransportFactory transportFactory)82 InternalAwsSecurityCredentialsSupplier( 83 AwsCredentialSource awsCredentialSource, 84 EnvironmentProvider environmentProvider, 85 HttpTransportFactory transportFactory) { 86 this.environmentProvider = environmentProvider; 87 this.awsCredentialSource = awsCredentialSource; 88 this.transportFactory = transportFactory; 89 } 90 91 @Override getCredentials(ExternalAccountSupplierContext context)92 public AwsSecurityCredentials getCredentials(ExternalAccountSupplierContext context) 93 throws IOException { 94 // Check environment variables for credentials first. 95 if (canRetrieveSecurityCredentialsFromEnvironment()) { 96 String accessKeyId = environmentProvider.getEnv(AWS_ACCESS_KEY_ID); 97 String secretAccessKey = environmentProvider.getEnv(AWS_SECRET_ACCESS_KEY); 98 String token = environmentProvider.getEnv(AWS_SESSION_TOKEN); 99 return new AwsSecurityCredentials(accessKeyId, secretAccessKey, token); 100 } 101 102 Map<String, Object> metadataRequestHeaders = createMetadataRequestHeaders(awsCredentialSource); 103 104 // Credentials not retrievable from environment variables - call metadata server. 105 // Retrieve the IAM role that is attached to the VM. This is required to retrieve the AWS 106 // security credentials. 107 if (awsCredentialSource.url == null || awsCredentialSource.url.isEmpty()) { 108 throw new IOException( 109 "Unable to determine the AWS IAM role name. The credential source does not contain the" 110 + " url field."); 111 } 112 String roleName = retrieveResource(awsCredentialSource.url, "IAM role", metadataRequestHeaders); 113 114 // Retrieve the AWS security credentials by calling the endpoint specified by the credential 115 // source. 116 String awsCredentials = 117 retrieveResource( 118 awsCredentialSource.url + "/" + roleName, "credentials", metadataRequestHeaders); 119 120 JsonParser parser = OAuth2Utils.JSON_FACTORY.createJsonParser(awsCredentials); 121 GenericJson genericJson = parser.parseAndClose(GenericJson.class); 122 123 String accessKeyId = (String) genericJson.get("AccessKeyId"); 124 String secretAccessKey = (String) genericJson.get("SecretAccessKey"); 125 String token = (String) genericJson.get("Token"); 126 127 // These credentials last for a few hours - we may consider caching these in the 128 // future. 129 return new AwsSecurityCredentials(accessKeyId, secretAccessKey, token); 130 } 131 132 @Override getRegion(ExternalAccountSupplierContext context)133 public String getRegion(ExternalAccountSupplierContext context) throws IOException { 134 String region; 135 if (canRetrieveRegionFromEnvironment()) { 136 // For AWS Lambda, the region is retrieved through the AWS_REGION environment variable. 137 region = environmentProvider.getEnv(AWS_REGION); 138 if (region != null && region.trim().length() > 0) { 139 return region; 140 } 141 return environmentProvider.getEnv(AWS_DEFAULT_REGION); 142 } 143 144 Map<String, Object> metadataRequestHeaders = createMetadataRequestHeaders(awsCredentialSource); 145 146 if (awsCredentialSource.regionUrl == null || awsCredentialSource.regionUrl.isEmpty()) { 147 throw new IOException( 148 "Unable to determine the AWS region. The credential source does not contain the region URL."); 149 } 150 151 region = retrieveResource(awsCredentialSource.regionUrl, "region", metadataRequestHeaders); 152 153 // There is an extra appended character that must be removed. If `us-east-1b` is returned, 154 // we want `us-east-1`. 155 return region.substring(0, region.length() - 1); 156 } 157 canRetrieveRegionFromEnvironment()158 private boolean canRetrieveRegionFromEnvironment() { 159 // The AWS region can be provided through AWS_REGION or AWS_DEFAULT_REGION. Only one is 160 // required. 161 List<String> keys = ImmutableList.of(AWS_REGION, AWS_DEFAULT_REGION); 162 for (String env : keys) { 163 String value = environmentProvider.getEnv(env); 164 if (value != null && value.trim().length() > 0) { 165 // Region available. 166 return true; 167 } 168 } 169 return false; 170 } 171 canRetrieveSecurityCredentialsFromEnvironment()172 private boolean canRetrieveSecurityCredentialsFromEnvironment() { 173 // Check if both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are available. 174 List<String> keys = ImmutableList.of(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY); 175 for (String env : keys) { 176 String value = environmentProvider.getEnv(env); 177 if (value == null || value.trim().length() == 0) { 178 // Return false if one of them are missing. 179 return false; 180 } 181 } 182 return true; 183 } 184 185 @VisibleForTesting shouldUseMetadataServer()186 boolean shouldUseMetadataServer() { 187 return (!canRetrieveRegionFromEnvironment() 188 || !canRetrieveSecurityCredentialsFromEnvironment()); 189 } 190 retrieveResource(String url, String resourceName, Map<String, Object> headers)191 private String retrieveResource(String url, String resourceName, Map<String, Object> headers) 192 throws IOException { 193 return retrieveResource(url, resourceName, HttpMethods.GET, headers, /* content= */ null); 194 } 195 retrieveResource( String url, String resourceName, String requestMethod, Map<String, Object> headers, @Nullable HttpContent content)196 private String retrieveResource( 197 String url, 198 String resourceName, 199 String requestMethod, 200 Map<String, Object> headers, 201 @Nullable HttpContent content) 202 throws IOException { 203 try { 204 HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory(); 205 HttpRequest request = 206 requestFactory.buildRequest(requestMethod, new GenericUrl(url), content); 207 208 HttpHeaders requestHeaders = request.getHeaders(); 209 for (Map.Entry<String, Object> header : headers.entrySet()) { 210 requestHeaders.set(header.getKey(), header.getValue()); 211 } 212 213 HttpResponse response = request.execute(); 214 return response.parseAsString(); 215 } catch (IOException e) { 216 throw new IOException(String.format("Failed to retrieve AWS %s.", resourceName), e); 217 } 218 } 219 220 @VisibleForTesting createMetadataRequestHeaders(AwsCredentialSource awsCredentialSource)221 Map<String, Object> createMetadataRequestHeaders(AwsCredentialSource awsCredentialSource) 222 throws IOException { 223 Map<String, Object> metadataRequestHeaders = new HashMap<>(); 224 225 // AWS IDMSv2 introduced a requirement for a session token to be present 226 // with the requests made to metadata endpoints. This requirement is to help 227 // prevent SSRF attacks. 228 // Presence of "imdsv2_session_token_url" in Credential Source of config file 229 // will trigger a flow with session token, else there will not be a session 230 // token with the metadata requests. 231 // Both flows work for IDMS v1 and v2. But if IDMSv2 is enabled, then if 232 // session token is not present, Unauthorized exception will be thrown. 233 if (awsCredentialSource.imdsv2SessionTokenUrl != null) { 234 Map<String, Object> tokenRequestHeaders = 235 new HashMap<String, Object>() { 236 { 237 put(AWS_IMDSV2_SESSION_TOKEN_TTL_HEADER, AWS_IMDSV2_SESSION_TOKEN_TTL); 238 } 239 }; 240 241 String imdsv2SessionToken = 242 retrieveResource( 243 awsCredentialSource.imdsv2SessionTokenUrl, 244 "Session Token", 245 HttpMethods.PUT, 246 tokenRequestHeaders, 247 /* content= */ null); 248 249 metadataRequestHeaders.put(AWS_IMDSV2_SESSION_TOKEN_HEADER, imdsv2SessionToken); 250 } 251 252 return metadataRequestHeaders; 253 } 254 } 255