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