• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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