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