• 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 org.junit.Assert.assertEquals;
35 import static org.junit.Assert.assertNotNull;
36 import static org.junit.Assert.assertTrue;
37 
38 import com.google.api.client.http.LowLevelHttpRequest;
39 import com.google.api.client.http.LowLevelHttpResponse;
40 import com.google.api.client.json.GenericJson;
41 import com.google.api.client.json.Json;
42 import com.google.api.client.json.JsonFactory;
43 import com.google.api.client.json.gson.GsonFactory;
44 import com.google.api.client.testing.http.MockHttpTransport;
45 import com.google.api.client.testing.http.MockLowLevelHttpRequest;
46 import com.google.api.client.testing.http.MockLowLevelHttpResponse;
47 import com.google.auth.TestUtils;
48 import com.google.common.base.Joiner;
49 import java.io.IOException;
50 import java.util.ArrayDeque;
51 import java.util.ArrayList;
52 import java.util.Collections;
53 import java.util.List;
54 import java.util.Map;
55 import java.util.Queue;
56 
57 /**
58  * Mock transport that handles the necessary steps to exchange an external credential for a GCP
59  * access-token.
60  */
61 public class MockExternalAccountCredentialsTransport extends MockHttpTransport {
62 
63   private static final String EXPECTED_GRANT_TYPE =
64       "urn:ietf:params:oauth:grant-type:token-exchange";
65   private static final String CLOUD_PLATFORM_SCOPE =
66       "https://www.googleapis.com/auth/cloud-platform";
67   private static final String ISSUED_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
68   private static final String AWS_CREDENTIALS_URL = "https://169.254.169.254";
69   private static final String AWS_REGION_URL = "https://169.254.169.254/region";
70   private static final String AWS_IMDSV2_SESSION_TOKEN_URL = "https://169.254.169.254/imdsv2";
71   private static final String METADATA_SERVER_URL = "https://www.metadata.google.com";
72   private static final String STS_URL = "https://sts.googleapis.com/v1/token";
73 
74   private static final String SUBJECT_TOKEN = "subjectToken";
75   private static final String TOKEN_TYPE = "Bearer";
76   private static final String ACCESS_TOKEN = "accessToken";
77   private static final String AWS_IMDSV2_SESSION_TOKEN = "sessiontoken";
78   private static final String SERVICE_ACCOUNT_ACCESS_TOKEN = "serviceAccountAccessToken";
79   private static final String AWS_REGION = "us-east-1b";
80   private static final Long EXPIRES_IN = 3600L;
81 
82   private static final JsonFactory JSON_FACTORY = new GsonFactory();
83 
84   static final String SERVICE_ACCOUNT_IMPERSONATION_URL =
85       "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/testn@test.iam.gserviceaccount.com:generateAccessToken";
86 
87   static final String IAM_ENDPOINT = "https://iamcredentials.googleapis.com";
88 
89   private Queue<Boolean> responseSequence = new ArrayDeque<>();
90   private Queue<IOException> responseErrorSequence = new ArrayDeque<>();
91   private Queue<String> refreshTokenSequence = new ArrayDeque<>();
92   private Queue<List<String>> scopeSequence = new ArrayDeque<>();
93   private List<MockLowLevelHttpRequest> requests = new ArrayList<>();
94   private String expireTime;
95   private String metadataServerContentType;
96   private String stsContent;
97 
addResponseErrorSequence(IOException... errors)98   public void addResponseErrorSequence(IOException... errors) {
99     Collections.addAll(responseErrorSequence, errors);
100   }
101 
addResponseSequence(Boolean... responses)102   public void addResponseSequence(Boolean... responses) {
103     Collections.addAll(responseSequence, responses);
104   }
105 
addRefreshTokenSequence(String... refreshTokens)106   public void addRefreshTokenSequence(String... refreshTokens) {
107     Collections.addAll(refreshTokenSequence, refreshTokens);
108   }
109 
addScopeSequence(List<String>.... scopes)110   public void addScopeSequence(List<String>... scopes) {
111     Collections.addAll(scopeSequence, scopes);
112   }
113 
114   @Override
115   @SuppressWarnings("unchecked")
buildRequest(final String method, final String url)116   public LowLevelHttpRequest buildRequest(final String method, final String url) {
117     MockLowLevelHttpRequest request =
118         new MockLowLevelHttpRequest(url) {
119           @Override
120           public LowLevelHttpResponse execute() throws IOException {
121             boolean successfulResponse = !responseSequence.isEmpty() && responseSequence.poll();
122 
123             if (!responseErrorSequence.isEmpty() && !successfulResponse) {
124               throw responseErrorSequence.poll();
125             }
126 
127             if (AWS_IMDSV2_SESSION_TOKEN_URL.equals(url)) {
128               return new MockLowLevelHttpResponse()
129                   .setContentType("text/html")
130                   .setContent(AWS_IMDSV2_SESSION_TOKEN);
131             }
132             if (AWS_REGION_URL.equals(url)) {
133               return new MockLowLevelHttpResponse()
134                   .setContentType("text/html")
135                   .setContent(AWS_REGION);
136             }
137             if (AWS_CREDENTIALS_URL.equals(url)) {
138               return new MockLowLevelHttpResponse()
139                   .setContentType("text/html")
140                   .setContent("roleName");
141             }
142             if ((AWS_CREDENTIALS_URL + "/" + "roleName").equals(url)) {
143               GenericJson response = new GenericJson();
144               response.setFactory(JSON_FACTORY);
145               response.put("AccessKeyId", "accessKeyId");
146               response.put("SecretAccessKey", "secretAccessKey");
147               response.put("Token", "token");
148 
149               return new MockLowLevelHttpResponse()
150                   .setContentType(Json.MEDIA_TYPE)
151                   .setContent(response.toString());
152             }
153 
154             if (METADATA_SERVER_URL.equals(url)) {
155               String metadataRequestHeader = getFirstHeaderValue("Metadata-Flavor");
156               if (!"Google".equals(metadataRequestHeader)) {
157                 throw new IOException("Metadata request header not found.");
158               }
159 
160               if (metadataServerContentType != null && metadataServerContentType.equals("json")) {
161                 GenericJson response = new GenericJson();
162                 response.setFactory(JSON_FACTORY);
163                 response.put("subjectToken", SUBJECT_TOKEN);
164                 return new MockLowLevelHttpResponse()
165                     .setContentType(Json.MEDIA_TYPE)
166                     .setContent(response.toString());
167               }
168               return new MockLowLevelHttpResponse()
169                   .setContentType("text/html")
170                   .setContent(SUBJECT_TOKEN);
171             }
172             if (STS_URL.equals(url)) {
173               Map<String, String> query = TestUtils.parseQuery(getContentAsString());
174 
175               // Store STS content as multiple calls are made using this transport.
176               stsContent = getContentAsString();
177 
178               assertEquals(EXPECTED_GRANT_TYPE, query.get("grant_type"));
179               assertNotNull(query.get("subject_token_type"));
180               assertNotNull(query.get("subject_token"));
181 
182               GenericJson response = new GenericJson();
183               response.setFactory(JSON_FACTORY);
184               response.put("token_type", TOKEN_TYPE);
185               response.put("expires_in", EXPIRES_IN);
186               response.put("access_token", ACCESS_TOKEN);
187               response.put("issued_token_type", ISSUED_TOKEN_TYPE);
188 
189               if (!refreshTokenSequence.isEmpty()) {
190                 response.put("refresh_token", refreshTokenSequence.poll());
191               }
192               if (!scopeSequence.isEmpty()) {
193                 response.put("scope", Joiner.on(' ').join(scopeSequence.poll()));
194               }
195               return new MockLowLevelHttpResponse()
196                   .setContentType(Json.MEDIA_TYPE)
197                   .setContent(response.toPrettyString());
198             }
199 
200             if (url.contains(IAM_ENDPOINT)) {
201               GenericJson query =
202                   OAuth2Utils.JSON_FACTORY
203                       .createJsonParser(getContentAsString())
204                       .parseAndClose(GenericJson.class);
205               assertEquals(CLOUD_PLATFORM_SCOPE, ((ArrayList<String>) query.get("scope")).get(0));
206               assertEquals(1, getHeaders().get("authorization").size());
207               assertTrue(getHeaders().containsKey("authorization"));
208               assertNotNull(getHeaders().get("authorization").get(0));
209 
210               GenericJson response = new GenericJson();
211               response.setFactory(JSON_FACTORY);
212               response.put("accessToken", SERVICE_ACCOUNT_ACCESS_TOKEN);
213               response.put("expireTime", expireTime);
214 
215               return new MockLowLevelHttpResponse()
216                   .setContentType(Json.MEDIA_TYPE)
217                   .setContent(response.toPrettyString());
218             }
219             return null;
220           }
221         };
222 
223     this.requests.add(request);
224     return request;
225   }
226 
getStsContent()227   public String getStsContent() {
228     return stsContent;
229   }
230 
getLastRequest()231   public MockLowLevelHttpRequest getLastRequest() {
232     if (requests.isEmpty()) {
233       return null;
234     }
235 
236     return requests.get(requests.size() - 1);
237   }
238 
getRequests()239   public List<MockLowLevelHttpRequest> getRequests() {
240     return Collections.unmodifiableList(requests);
241   }
242 
getTokenType()243   public String getTokenType() {
244     return TOKEN_TYPE;
245   }
246 
getAccessToken()247   public String getAccessToken() {
248     return ACCESS_TOKEN;
249   }
250 
getServiceAccountAccessToken()251   public String getServiceAccountAccessToken() {
252     return SERVICE_ACCOUNT_ACCESS_TOKEN;
253   }
254 
getIssuedTokenType()255   public String getIssuedTokenType() {
256     return ISSUED_TOKEN_TYPE;
257   }
258 
getExpiresIn()259   public Long getExpiresIn() {
260     return EXPIRES_IN;
261   }
262 
getSubjectToken()263   public String getSubjectToken() {
264     return SUBJECT_TOKEN;
265   }
266 
getMetadataUrl()267   public String getMetadataUrl() {
268     return METADATA_SERVER_URL;
269   }
270 
getAwsCredentialsUrl()271   public String getAwsCredentialsUrl() {
272     return AWS_CREDENTIALS_URL;
273   }
274 
getAwsRegionUrl()275   public String getAwsRegionUrl() {
276     return AWS_REGION_URL;
277   }
278 
getAwsImdsv2SessionTokenUrl()279   public String getAwsImdsv2SessionTokenUrl() {
280     return AWS_IMDSV2_SESSION_TOKEN_URL;
281   }
282 
getAwsRegion()283   public String getAwsRegion() {
284     return AWS_REGION;
285   }
286 
getStsUrl()287   public String getStsUrl() {
288     return STS_URL;
289   }
290 
getServiceAccountImpersonationUrl()291   public String getServiceAccountImpersonationUrl() {
292     return SERVICE_ACCOUNT_IMPERSONATION_URL;
293   }
294 
setExpireTime(String expireTime)295   public void setExpireTime(String expireTime) {
296     this.expireTime = expireTime;
297   }
298 
setMetadataServerContentType(String contentType)299   public void setMetadataServerContentType(String contentType) {
300     this.metadataServerContentType = contentType;
301   }
302 }
303