1 /* 2 * Copyright 2015, Google Inc. All rights reserved. 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 Inc. 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.LowLevelHttpRequest; 35 import com.google.api.client.http.LowLevelHttpResponse; 36 import com.google.api.client.json.GenericJson; 37 import com.google.api.client.json.Json; 38 import com.google.api.client.json.JsonFactory; 39 import com.google.api.client.json.gson.GsonFactory; 40 import com.google.api.client.json.webtoken.JsonWebSignature; 41 import com.google.api.client.testing.http.MockHttpTransport; 42 import com.google.api.client.testing.http.MockLowLevelHttpRequest; 43 import com.google.api.client.testing.http.MockLowLevelHttpResponse; 44 import com.google.auth.TestUtils; 45 import com.google.common.util.concurrent.Futures; 46 import java.io.IOException; 47 import java.net.URI; 48 import java.util.ArrayDeque; 49 import java.util.Collections; 50 import java.util.HashMap; 51 import java.util.Map; 52 import java.util.Queue; 53 import java.util.concurrent.ExecutionException; 54 import java.util.concurrent.Future; 55 56 /** Mock transport to simulate providing Google OAuth2 access tokens */ 57 public class MockTokenServerTransport extends MockHttpTransport { 58 59 public static final String REFRESH_TOKEN_WITH_USER_SCOPE = "refresh_token_with_user.email_scope"; 60 static final JsonFactory JSON_FACTORY = new GsonFactory(); 61 int buildRequestCount; 62 final Map<String, String> clients = new HashMap<String, String>(); 63 final Map<String, String> refreshTokens = new HashMap<String, String>(); 64 final Map<String, String> grantedScopes = new HashMap<String, String>(); 65 final Map<String, String> serviceAccounts = new HashMap<String, String>(); 66 final Map<String, String> gdchServiceAccounts = new HashMap<String, String>(); 67 final Map<String, String> codes = new HashMap<String, String>(); 68 final Map<String, Map<String, String>> additionalParameters = 69 new HashMap<String, Map<String, String>>(); 70 URI tokenServerUri = OAuth2Utils.TOKEN_SERVER_URI; 71 private IOException error; 72 private final Queue<Future<LowLevelHttpResponse>> responseSequence = new ArrayDeque<>(); 73 private int expiresInSeconds = 3600; 74 MockTokenServerTransport()75 public MockTokenServerTransport() {} 76 getTokenServerUri()77 public URI getTokenServerUri() { 78 return tokenServerUri; 79 } 80 setTokenServerUri(URI tokenServerUri)81 public void setTokenServerUri(URI tokenServerUri) { 82 this.tokenServerUri = tokenServerUri; 83 } 84 addAuthorizationCode( String code, String refreshToken, String accessToken, String grantedScopes, Map<String, String> additionalParameters)85 public void addAuthorizationCode( 86 String code, 87 String refreshToken, 88 String accessToken, 89 String grantedScopes, 90 Map<String, String> additionalParameters) { 91 codes.put(code, refreshToken); 92 refreshTokens.put(refreshToken, accessToken); 93 this.grantedScopes.put(refreshToken, grantedScopes); 94 95 if (additionalParameters != null) { 96 this.additionalParameters.put(refreshToken, additionalParameters); 97 } 98 } 99 addClient(String clientId, String clientSecret)100 public void addClient(String clientId, String clientSecret) { 101 clients.put(clientId, clientSecret); 102 } 103 addRefreshToken(String refreshToken, String accessTokenToReturn)104 public void addRefreshToken(String refreshToken, String accessTokenToReturn) { 105 refreshTokens.put(refreshToken, accessTokenToReturn); 106 } 107 addRefreshToken( String refreshToken, String accessTokenToReturn, String grantedScopes)108 public void addRefreshToken( 109 String refreshToken, String accessTokenToReturn, String grantedScopes) { 110 refreshTokens.put(refreshToken, accessTokenToReturn); 111 this.grantedScopes.put(refreshToken, grantedScopes); 112 } 113 addServiceAccount(String email, String accessToken)114 public void addServiceAccount(String email, String accessToken) { 115 serviceAccounts.put(email, accessToken); 116 } 117 addGdchServiceAccount(String serviceIdentityName, String accessToken)118 public void addGdchServiceAccount(String serviceIdentityName, String accessToken) { 119 gdchServiceAccounts.put(serviceIdentityName, accessToken); 120 } 121 getAccessToken(String refreshToken)122 public String getAccessToken(String refreshToken) { 123 return refreshTokens.get(refreshToken); 124 } 125 setError(IOException error)126 public void setError(IOException error) { 127 this.error = error; 128 } 129 addResponseErrorSequence(IOException... errors)130 public void addResponseErrorSequence(IOException... errors) { 131 for (IOException error : errors) { 132 responseSequence.add(Futures.<LowLevelHttpResponse>immediateFailedFuture(error)); 133 } 134 } 135 addResponseSequence(LowLevelHttpResponse... responses)136 public void addResponseSequence(LowLevelHttpResponse... responses) { 137 for (LowLevelHttpResponse response : responses) { 138 responseSequence.add(Futures.immediateFuture(response)); 139 } 140 } 141 addResponseSequence(Future<LowLevelHttpResponse> response)142 public void addResponseSequence(Future<LowLevelHttpResponse> response) { 143 responseSequence.add(response); 144 } 145 setExpiresInSeconds(int expiresInSeconds)146 public void setExpiresInSeconds(int expiresInSeconds) { 147 this.expiresInSeconds = expiresInSeconds; 148 } 149 150 @Override buildRequest(String method, String url)151 public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { 152 buildRequestCount++; 153 if (error != null) { 154 throw error; 155 } 156 int questionMarkPos = url.indexOf('?'); 157 final String urlWithoutQuery = (questionMarkPos > 0) ? url.substring(0, questionMarkPos) : url; 158 final String query = (questionMarkPos > 0) ? url.substring(questionMarkPos + 1) : ""; 159 160 if (!responseSequence.isEmpty()) { 161 return new MockLowLevelHttpRequest(url) { 162 @Override 163 public LowLevelHttpResponse execute() throws IOException { 164 try { 165 return responseSequence.poll().get(); 166 } catch (ExecutionException e) { 167 Throwable cause = e.getCause(); 168 throw (IOException) cause; 169 } catch (InterruptedException e) { 170 Thread.currentThread().interrupt(); 171 throw new RuntimeException("Unexpectedly interrupted"); 172 } 173 } 174 }; 175 } 176 177 if (urlWithoutQuery.equals(tokenServerUri.toString())) { 178 return new MockLowLevelHttpRequest(url) { 179 @Override 180 public LowLevelHttpResponse execute() throws IOException { 181 182 if (!responseSequence.isEmpty()) { 183 try { 184 return responseSequence.poll().get(); 185 } catch (ExecutionException e) { 186 Throwable cause = e.getCause(); 187 throw (IOException) cause; 188 } catch (InterruptedException e) { 189 Thread.currentThread().interrupt(); 190 throw new RuntimeException("Unexpectedly interrupted"); 191 } 192 } 193 194 String content = this.getContentAsString(); 195 Map<String, String> query = TestUtils.parseQuery(content); 196 String accessToken = null; 197 String refreshToken = null; 198 String grantedScopesString = null; 199 boolean generateAccessToken = true; 200 201 String foundId = query.get("client_id"); 202 boolean isUserEmailScope = false; 203 if (foundId != null) { 204 if (!clients.containsKey(foundId)) { 205 throw new IOException("Client ID not found."); 206 } 207 String foundSecret = query.get("client_secret"); 208 String expectedSecret = clients.get(foundId); 209 if (foundSecret == null || !foundSecret.equals(expectedSecret)) { 210 throw new IOException("Client secret not found."); 211 } 212 String grantType = query.get("grant_type"); 213 if (grantType != null && grantType.equals("authorization_code")) { 214 String foundCode = query.get("code"); 215 if (!codes.containsKey(foundCode)) { 216 throw new IOException("Authorization code not found"); 217 } 218 refreshToken = codes.get(foundCode); 219 } else { 220 refreshToken = query.get("refresh_token"); 221 } 222 if (!refreshTokens.containsKey(refreshToken)) { 223 throw new IOException("Refresh Token not found."); 224 } 225 if (refreshToken.equals(REFRESH_TOKEN_WITH_USER_SCOPE)) { 226 isUserEmailScope = true; 227 } 228 accessToken = refreshTokens.get(refreshToken); 229 230 if (grantedScopes.containsKey(refreshToken)) { 231 grantedScopesString = grantedScopes.get(refreshToken); 232 } 233 234 if (additionalParameters.containsKey(refreshToken)) { 235 Map<String, String> additionalParametersMap = additionalParameters.get(refreshToken); 236 for (Map.Entry<String, String> entry : additionalParametersMap.entrySet()) { 237 String key = entry.getKey(); 238 String expectedValue = entry.getValue(); 239 if (!query.containsKey(key)) { 240 throw new IllegalArgumentException("Missing additional parameter: " + key); 241 } else { 242 String actualValue = query.get(key); 243 if (!expectedValue.equals(actualValue)) { 244 throw new IllegalArgumentException( 245 "For additional parameter " 246 + key 247 + ", Actual value: " 248 + actualValue 249 + ", Expected value: " 250 + expectedValue); 251 } 252 } 253 } 254 } 255 256 } else if (query.containsKey("grant_type")) { 257 String grantType = query.get("grant_type"); 258 String assertion = query.get("assertion"); 259 JsonWebSignature signature = JsonWebSignature.parse(JSON_FACTORY, assertion); 260 if (OAuth2Utils.GRANT_TYPE_JWT_BEARER.equals(grantType)) { 261 String foundEmail = signature.getPayload().getIssuer(); 262 if (!serviceAccounts.containsKey(foundEmail)) {} 263 accessToken = serviceAccounts.get(foundEmail); 264 String foundTargetAudience = (String) signature.getPayload().get("target_audience"); 265 String foundScopes = (String) signature.getPayload().get("scope"); 266 if ((foundScopes == null || foundScopes.length() == 0) 267 && (foundTargetAudience == null || foundTargetAudience.length() == 0)) { 268 throw new IOException("Either target_audience or scopes must be specified."); 269 } 270 271 if (foundScopes != null && foundTargetAudience != null) { 272 throw new IOException("Only one of target_audience or scopes must be specified."); 273 } 274 if (foundTargetAudience != null) { 275 generateAccessToken = false; 276 } 277 278 // For GDCH scenario 279 } else if (OAuth2Utils.TOKEN_TYPE_TOKEN_EXCHANGE.equals(grantType)) { 280 String foundServiceIdentityName = signature.getPayload().getIssuer(); 281 if (!gdchServiceAccounts.containsKey(foundServiceIdentityName)) { 282 throw new IOException( 283 "GDCH Service Account Service Identity Name not found as issuer."); 284 } 285 accessToken = gdchServiceAccounts.get(foundServiceIdentityName); 286 String foundApiAudience = (String) signature.getPayload().get("api_audience"); 287 if ((foundApiAudience == null || foundApiAudience.length() == 0)) { 288 throw new IOException("Api_audience must be specified."); 289 } 290 } else { 291 throw new IOException("Service Account Email not found as issuer."); 292 } 293 } else { 294 throw new IOException("Unknown token type."); 295 } 296 297 // Create the JSON response 298 // https://developers.google.com/identity/protocols/OpenIDConnect#server-flow 299 GenericJson responseContents = new GenericJson(); 300 responseContents.setFactory(JSON_FACTORY); 301 responseContents.put("token_type", "Bearer"); 302 responseContents.put("expires_in", expiresInSeconds); 303 if (generateAccessToken) { 304 responseContents.put("access_token", accessToken); 305 if (refreshToken != null) { 306 responseContents.put("refresh_token", refreshToken); 307 } 308 if (grantedScopesString != null) { 309 responseContents.put("scope", grantedScopesString); 310 } 311 } 312 if (isUserEmailScope || !generateAccessToken) { 313 responseContents.put("id_token", ServiceAccountCredentialsTest.DEFAULT_ID_TOKEN); 314 } 315 String refreshText = responseContents.toPrettyString(); 316 317 return new MockLowLevelHttpResponse() 318 .setContentType(Json.MEDIA_TYPE) 319 .setContent(refreshText); 320 } 321 }; 322 } else if (urlWithoutQuery.equals(OAuth2Utils.TOKEN_REVOKE_URI.toString())) { 323 return new MockLowLevelHttpRequest(url) { 324 @Override 325 public LowLevelHttpResponse execute() throws IOException { 326 Map<String, String> parameters = TestUtils.parseQuery(this.getContentAsString()); 327 String token = parameters.get("token"); 328 if (token == null) { 329 throw new IOException("Token to revoke not found."); 330 } 331 // Token could be access token or refresh token so remove keys and values 332 refreshTokens.values().removeAll(Collections.singleton(token)); 333 refreshTokens.remove(token); 334 return new MockLowLevelHttpResponse().setContentType(Json.MEDIA_TYPE); 335 } 336 }; 337 } 338 return super.buildRequest(method, url); 339 } 340 } 341