• 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 com.google.auth.Credentials.GOOGLE_DEFAULT_UNIVERSE;
35 import static org.junit.Assert.assertEquals;
36 import static org.junit.Assert.assertNotNull;
37 import static org.junit.Assert.fail;
38 
39 import com.google.api.client.http.HttpTransport;
40 import com.google.auth.TestUtils;
41 import com.google.auth.http.HttpTransportFactory;
42 import java.io.IOException;
43 import java.util.Date;
44 import java.util.Map;
45 import org.junit.Test;
46 import org.junit.runner.RunWith;
47 import org.junit.runners.JUnit4;
48 
49 /** Tests for {@link DownscopedCredentials}. */
50 @RunWith(JUnit4.class)
51 public class DownscopedCredentialsTest {
52 
53   private final String TOKEN_EXCHANGE_URL_FORMAT = "https://sts.%s/v1/token";
54   private static final String SA_PRIVATE_KEY_PKCS8 =
55       "-----BEGIN PRIVATE KEY-----\n"
56           + "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALX0PQoe1igW12i"
57           + "kv1bN/r9lN749y2ijmbc/mFHPyS3hNTyOCjDvBbXYbDhQJzWVUikh4mvGBA07qTj79Xc3yBDfKP2IeyYQIFe0t0"
58           + "zkd7R9Zdn98Y2rIQC47aAbDfubtkU1U72t4zL11kHvoa0/RuFZjncvlr42X7be7lYh4p3NAgMBAAECgYASk5wDw"
59           + "4Az2ZkmeuN6Fk/y9H+Lcb2pskJIXjrL533vrDWGOC48LrsThMQPv8cxBky8HFSEklPpkfTF95tpD43iVwJRB/Gr"
60           + "CtGTw65IfJ4/tI09h6zGc4yqvIo1cHX/LQ+SxKLGyir/dQM925rGt/VojxY5ryJR7GLbCzxPnJm/oQJBANwOCO6"
61           + "D2hy1LQYJhXh7O+RLtA/tSnT1xyMQsGT+uUCMiKS2bSKx2wxo9k7h3OegNJIu1q6nZ6AbxDK8H3+d0dUCQQDTrP"
62           + "SXagBxzp8PecbaCHjzNRSQE2in81qYnrAFNB4o3DpHyMMY6s5ALLeHKscEWnqP8Ur6X4PvzZecCWU9BKAZAkAut"
63           + "LPknAuxSCsUOvUfS1i87ex77Ot+w6POp34pEX+UWb+u5iFn2cQacDTHLV1LtE80L8jVLSbrbrlH43H0DjU5AkEA"
64           + "gidhycxS86dxpEljnOMCw8CKoUBd5I880IUahEiUltk7OLJYS/Ts1wbn3kPOVX3wyJs8WBDtBkFrDHW2ezth2QJ"
65           + "ADj3e1YhMVdjJW5jqwlD/VNddGjgzyunmiZg0uOXsHXbytYmsA545S8KRQFaJKFXYYFo2kOjqOiC1T2cAzMDjCQ"
66           + "==\n-----END PRIVATE KEY-----\n";
67 
68   private static final CredentialAccessBoundary CREDENTIAL_ACCESS_BOUNDARY =
69       CredentialAccessBoundary.newBuilder()
70           .addRule(
71               CredentialAccessBoundary.AccessBoundaryRule.newBuilder()
72                   .setAvailableResource("//storage.googleapis.com/projects/_/buckets/bucket")
73                   .addAvailablePermission("inRole:roles/storage.objectViewer")
74                   .build())
75           .build();
76 
77   static class MockStsTransportFactory implements HttpTransportFactory {
78 
79     MockStsTransport transport = new MockStsTransport();
80 
81     @Override
create()82     public HttpTransport create() {
83       return transport;
84     }
85   }
86 
87   @Test
refreshAccessToken()88   public void refreshAccessToken() throws IOException {
89     MockStsTransportFactory transportFactory = new MockStsTransportFactory();
90 
91     GoogleCredentials sourceCredentials =
92         getServiceAccountSourceCredentials(/* canRefresh= */ true);
93 
94     DownscopedCredentials downscopedCredentials =
95         DownscopedCredentials.newBuilder()
96             .setSourceCredential(sourceCredentials)
97             .setCredentialAccessBoundary(CREDENTIAL_ACCESS_BOUNDARY)
98             .setHttpTransportFactory(transportFactory)
99             .build();
100 
101     AccessToken accessToken = downscopedCredentials.refreshAccessToken();
102 
103     assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());
104 
105     // Validate CAB specific params.
106     Map<String, String> query =
107         TestUtils.parseQuery(transportFactory.transport.getRequest().getContentAsString());
108     assertNotNull(query.get("options"));
109     assertEquals(CREDENTIAL_ACCESS_BOUNDARY.toJson(), query.get("options"));
110     assertEquals(
111         "urn:ietf:params:oauth:token-type:access_token", query.get("requested_token_type"));
112 
113     // Verify domain.
114     String url = transportFactory.transport.getRequest().getUrl();
115     assertEquals(url, String.format(TOKEN_EXCHANGE_URL_FORMAT, GOOGLE_DEFAULT_UNIVERSE));
116   }
117 
118   @Test
refreshAccessToken_withCustomUniverseDomain()119   public void refreshAccessToken_withCustomUniverseDomain() throws IOException {
120     MockStsTransportFactory transportFactory = new MockStsTransportFactory();
121     String universeDomain = "foobar";
122     GoogleCredentials sourceCredentials =
123         getServiceAccountSourceCredentials(/* canRefresh= */ true)
124             .toBuilder()
125             .setUniverseDomain(universeDomain)
126             .build();
127 
128     DownscopedCredentials downscopedCredentials =
129         DownscopedCredentials.newBuilder()
130             .setSourceCredential(sourceCredentials)
131             .setCredentialAccessBoundary(CREDENTIAL_ACCESS_BOUNDARY)
132             .setHttpTransportFactory(transportFactory)
133             .setUniverseDomain(universeDomain)
134             .build();
135 
136     AccessToken accessToken = downscopedCredentials.refreshAccessToken();
137 
138     assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());
139 
140     // Validate CAB specific params.
141     Map<String, String> query =
142         TestUtils.parseQuery(transportFactory.transport.getRequest().getContentAsString());
143     assertNotNull(query.get("options"));
144     assertEquals(CREDENTIAL_ACCESS_BOUNDARY.toJson(), query.get("options"));
145     assertEquals(
146         "urn:ietf:params:oauth:token-type:access_token", query.get("requested_token_type"));
147 
148     // Verify domain.
149     String url = transportFactory.transport.getRequest().getUrl();
150     assertEquals(url, String.format(TOKEN_EXCHANGE_URL_FORMAT, universeDomain));
151   }
152 
153   @Test
refreshAccessToken_userCredentials_expectExpiresInCopied()154   public void refreshAccessToken_userCredentials_expectExpiresInCopied() throws IOException {
155     // STS only returns expires_in if the source access token belongs to a service account.
156     // For other source credential types, we can copy the source credentials expiration as
157     // the generated downscoped token will always have the same expiration time as the source
158     // credentials.
159 
160     MockStsTransportFactory transportFactory = new MockStsTransportFactory();
161     transportFactory.transport.setReturnExpiresIn(false);
162 
163     GoogleCredentials sourceCredentials = getUserSourceCredentials();
164 
165     DownscopedCredentials downscopedCredentials =
166         DownscopedCredentials.newBuilder()
167             .setSourceCredential(sourceCredentials)
168             .setCredentialAccessBoundary(CREDENTIAL_ACCESS_BOUNDARY)
169             .setHttpTransportFactory(transportFactory)
170             .build();
171 
172     AccessToken accessToken = downscopedCredentials.refreshAccessToken();
173 
174     assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());
175 
176     // Validate that the expires_in has been copied from the source credential.
177     assertEquals(
178         sourceCredentials.getAccessToken().getExpirationTime(), accessToken.getExpirationTime());
179   }
180 
181   @Test
refreshAccessToken_cantRefreshSourceCredentials_throws()182   public void refreshAccessToken_cantRefreshSourceCredentials_throws() throws IOException {
183     MockStsTransportFactory transportFactory = new MockStsTransportFactory();
184 
185     GoogleCredentials sourceCredentials =
186         getServiceAccountSourceCredentials(/* canRefresh= */ false);
187 
188     DownscopedCredentials downscopedCredentials =
189         DownscopedCredentials.newBuilder()
190             .setSourceCredential(sourceCredentials)
191             .setCredentialAccessBoundary(CREDENTIAL_ACCESS_BOUNDARY)
192             .setHttpTransportFactory(transportFactory)
193             .build();
194 
195     try {
196       downscopedCredentials.refreshAccessToken();
197       fail("Should fail as the source credential should not be able to be refreshed.");
198     } catch (IOException e) {
199       assertEquals("Unable to refresh the provided source credential.", e.getMessage());
200     }
201   }
202 
203   @Test
builder_noSourceCredential_throws()204   public void builder_noSourceCredential_throws() {
205     try {
206       DownscopedCredentials.newBuilder()
207           .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
208           .setCredentialAccessBoundary(CREDENTIAL_ACCESS_BOUNDARY)
209           .build();
210       fail("Should fail as the source credential is null.");
211     } catch (NullPointerException e) {
212       // Expected.
213     }
214   }
215 
216   @Test
builder_noCredentialAccessBoundary_throws()217   public void builder_noCredentialAccessBoundary_throws() throws IOException {
218     try {
219       DownscopedCredentials.newBuilder()
220           .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
221           .setSourceCredential(getServiceAccountSourceCredentials(/* canRefresh= */ true))
222           .build();
223       fail("Should fail as no access boundary was provided.");
224     } catch (NullPointerException e) {
225       // Expected.
226     }
227   }
228 
229   @Test
builder_noTransport_defaults()230   public void builder_noTransport_defaults() throws IOException {
231     GoogleCredentials sourceCredentials =
232         getServiceAccountSourceCredentials(/* canRefresh= */ true);
233     DownscopedCredentials credentials =
234         DownscopedCredentials.newBuilder()
235             .setSourceCredential(sourceCredentials)
236             .setCredentialAccessBoundary(CREDENTIAL_ACCESS_BOUNDARY)
237             .build();
238 
239     GoogleCredentials scopedSourceCredentials =
240         sourceCredentials.createScoped("https://www.googleapis.com/auth/cloud-platform");
241     assertEquals(scopedSourceCredentials, credentials.getSourceCredentials());
242     assertEquals(CREDENTIAL_ACCESS_BOUNDARY, credentials.getCredentialAccessBoundary());
243     assertEquals(OAuth2Utils.HTTP_TRANSPORT_FACTORY, credentials.getTransportFactory());
244   }
245 
246   @Test
builder_noUniverseDomain_defaults()247   public void builder_noUniverseDomain_defaults() throws IOException {
248     GoogleCredentials sourceCredentials =
249         getServiceAccountSourceCredentials(/* canRefresh= */ true);
250     DownscopedCredentials credentials =
251         DownscopedCredentials.newBuilder()
252             .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
253             .setSourceCredential(sourceCredentials)
254             .setCredentialAccessBoundary(CREDENTIAL_ACCESS_BOUNDARY)
255             .build();
256 
257     GoogleCredentials scopedSourceCredentials =
258         sourceCredentials.createScoped("https://www.googleapis.com/auth/cloud-platform");
259     assertEquals(OAuth2Utils.HTTP_TRANSPORT_FACTORY, credentials.getTransportFactory());
260     assertEquals(scopedSourceCredentials, credentials.getSourceCredentials());
261     assertEquals(CREDENTIAL_ACCESS_BOUNDARY, credentials.getCredentialAccessBoundary());
262     assertEquals(GOOGLE_DEFAULT_UNIVERSE, credentials.getUniverseDomain());
263   }
264 
265   @Test
builder_universeDomainMismatch_throws()266   public void builder_universeDomainMismatch_throws() throws IOException {
267     GoogleCredentials sourceCredentials =
268         getServiceAccountSourceCredentials(/* canRefresh= */ true);
269 
270     try {
271       DownscopedCredentials.newBuilder()
272           .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
273           .setSourceCredential(sourceCredentials)
274           .setCredentialAccessBoundary(CREDENTIAL_ACCESS_BOUNDARY)
275           .setUniverseDomain("differentUniverseDomain")
276           .build();
277       fail("Should fail with universe domain mismatch.");
278     } catch (IllegalArgumentException e) {
279       assertEquals(
280           "The downscoped credential's universe domain must be the same as the source credential.",
281           e.getMessage());
282     }
283   }
284 
285   @Test
builder_sourceUniverseDomainUnavailable_throws()286   public void builder_sourceUniverseDomainUnavailable_throws() throws IOException {
287     GoogleCredentials sourceCredentials = new MockSourceCredentialWithoutUniverseDomain();
288 
289     try {
290       DownscopedCredentials.newBuilder()
291           .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
292           .setSourceCredential(sourceCredentials)
293           .setCredentialAccessBoundary(CREDENTIAL_ACCESS_BOUNDARY)
294           .build();
295       fail("Should fail to retrieve source credential universe domain.");
296     } catch (IllegalStateException e) {
297       assertEquals(
298           "Error occurred when attempting to retrieve source credential universe domain.",
299           e.getMessage());
300     }
301   }
302 
getServiceAccountSourceCredentials(boolean canRefresh)303   private static GoogleCredentials getServiceAccountSourceCredentials(boolean canRefresh)
304       throws IOException {
305     MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory();
306 
307     String email = "service-account@google.com";
308 
309     ServiceAccountCredentials sourceCredentials =
310         ServiceAccountCredentials.newBuilder()
311             .setClientEmail(email)
312             .setPrivateKey(OAuth2Utils.privateKeyFromPkcs8(SA_PRIVATE_KEY_PKCS8))
313             .setPrivateKeyId("privateKeyId")
314             .setProjectId("projectId")
315             .setHttpTransportFactory(transportFactory)
316             .build();
317 
318     transportFactory.transport.addServiceAccount(email, "accessToken");
319 
320     if (!canRefresh) {
321       transportFactory.transport.setError(new IOException());
322     }
323 
324     return sourceCredentials.createScoped("https://www.googleapis.com/auth/cloud-platform");
325   }
326 
getUserSourceCredentials()327   private static GoogleCredentials getUserSourceCredentials() {
328     MockTokenServerTransportFactory transportFactory = new MockTokenServerTransportFactory();
329     transportFactory.transport.addClient("clientId", "clientSecret");
330     transportFactory.transport.addRefreshToken("refreshToken", "accessToken");
331     AccessToken accessToken = new AccessToken("accessToken", new Date());
332     return UserCredentials.newBuilder()
333         .setClientId("clientId")
334         .setClientSecret("clientSecret")
335         .setRefreshToken("refreshToken")
336         .setAccessToken(accessToken)
337         .setHttpTransportFactory(transportFactory)
338         .build();
339   }
340 
341   static class MockSourceCredentialWithoutUniverseDomain extends GoogleCredentials {
342     @Override
getUniverseDomain()343     public String getUniverseDomain() throws IOException {
344       throw new IOException();
345     }
346   }
347 }
348