• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2022 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 com.google.auth.oauth2.MockExternalAccountCredentialsTransport.SERVICE_ACCOUNT_IMPERSONATION_URL;
36 import static org.junit.Assert.*;
37 
38 import com.google.api.client.http.HttpTransport;
39 import com.google.api.client.json.GenericJson;
40 import com.google.auth.TestUtils;
41 import com.google.auth.http.HttpTransportFactory;
42 import com.google.auth.oauth2.ExecutableHandler.ExecutableOptions;
43 import java.io.IOException;
44 import java.io.InputStream;
45 import java.io.NotSerializableException;
46 import java.math.BigDecimal;
47 import java.util.Arrays;
48 import java.util.HashMap;
49 import java.util.List;
50 import java.util.Map;
51 import javax.annotation.Nullable;
52 import org.junit.Test;
53 
54 /** Tests for {@link PluggableAuthCredentials}. */
55 public class PluggableAuthCredentialsTest extends BaseSerializationTest {
56   // The default timeout for waiting for the executable to finish (30 seconds).
57   private static final int DEFAULT_EXECUTABLE_TIMEOUT_MS = 30 * 1000;
58   // The minimum timeout for waiting for the executable to finish (5 seconds).
59   private static final int MINIMUM_EXECUTABLE_TIMEOUT_MS = 5 * 1000;
60   // The maximum timeout for waiting for the executable to finish (120 seconds).
61   private static final int MAXIMUM_EXECUTABLE_TIMEOUT_MS = 120 * 1000;
62   private static final String STS_URL = "https://sts.googleapis.com";
63 
64   private static final PluggableAuthCredentials CREDENTIAL =
65       PluggableAuthCredentials.newBuilder()
66           .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
67           .setAudience(
68               "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider")
69           .setSubjectTokenType("subjectTokenType")
70           .setTokenUrl(STS_URL)
71           .setTokenInfoUrl("tokenInfoUrl")
72           .setCredentialSource(buildCredentialSource())
73           .build();
74 
75   static class MockExternalAccountCredentialsTransportFactory implements HttpTransportFactory {
76 
77     MockExternalAccountCredentialsTransport transport =
78         new MockExternalAccountCredentialsTransport();
79 
80     @Override
create()81     public HttpTransport create() {
82       return transport;
83     }
84   }
85 
86   @Test
retrieveSubjectToken_shouldDelegateToHandler()87   public void retrieveSubjectToken_shouldDelegateToHandler() throws IOException {
88     PluggableAuthCredentials credential =
89         PluggableAuthCredentials.newBuilder(CREDENTIAL)
90             .setExecutableHandler(options -> "pluggableAuthToken")
91             .build();
92     String subjectToken = credential.retrieveSubjectToken();
93     assertEquals(subjectToken, "pluggableAuthToken");
94   }
95 
96   @Test
retrieveSubjectToken_shouldPassAllOptionsToHandler()97   public void retrieveSubjectToken_shouldPassAllOptionsToHandler() throws IOException {
98     String command = "/path/to/executable";
99     String timeout = "5000";
100     String outputFile = "/path/to/output/file";
101 
102     final ExecutableOptions[] providedOptions = {null};
103     ExecutableHandler executableHandler =
104         options -> {
105           providedOptions[0] = options;
106           return "pluggableAuthToken";
107         };
108 
109     PluggableAuthCredentials credential =
110         PluggableAuthCredentials.newBuilder(CREDENTIAL)
111             .setExecutableHandler(executableHandler)
112             .setCredentialSource(buildCredentialSource(command, timeout, outputFile))
113             .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
114             .build();
115 
116     String subjectToken = credential.retrieveSubjectToken();
117 
118     assertEquals(subjectToken, "pluggableAuthToken");
119 
120     // Validate that the correct options were passed to the executable handler.
121     ExecutableOptions options = providedOptions[0];
122     assertEquals(options.getExecutableCommand(), command);
123     assertEquals(options.getExecutableTimeoutMs(), Integer.parseInt(timeout));
124     assertEquals(options.getOutputFilePath(), outputFile);
125 
126     Map<String, String> envMap = options.getEnvironmentMap();
127     assertEquals(envMap.size(), 5);
128     assertEquals(envMap.get("GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"), credential.getAudience());
129     assertEquals(
130         envMap.get("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"), credential.getSubjectTokenType());
131     assertEquals(envMap.get("GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"), "0");
132     assertEquals(
133         envMap.get("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL"),
134         credential.getServiceAccountEmail());
135     assertEquals(envMap.get("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE"), outputFile);
136   }
137 
138   @Test
retrieveSubjectToken_shouldPassMinimalOptionsToHandler()139   public void retrieveSubjectToken_shouldPassMinimalOptionsToHandler() throws IOException {
140     String command = "/path/to/executable";
141 
142     final ExecutableOptions[] providedOptions = {null};
143     ExecutableHandler executableHandler =
144         options -> {
145           providedOptions[0] = options;
146           return "pluggableAuthToken";
147         };
148 
149     PluggableAuthCredentials credential =
150         PluggableAuthCredentials.newBuilder(CREDENTIAL)
151             .setExecutableHandler(executableHandler)
152             .setCredentialSource(
153                 buildCredentialSource(command, /* timeoutMs= */ null, /* outputFile= */ null))
154             .build();
155 
156     String subjectToken = credential.retrieveSubjectToken();
157 
158     assertEquals(subjectToken, "pluggableAuthToken");
159 
160     // Validate that the correct options were passed to the executable handler.
161     ExecutableOptions options = providedOptions[0];
162     assertEquals(options.getExecutableCommand(), command);
163     assertEquals(options.getExecutableTimeoutMs(), DEFAULT_EXECUTABLE_TIMEOUT_MS);
164     assertNull(options.getOutputFilePath());
165 
166     Map<String, String> envMap = options.getEnvironmentMap();
167     assertEquals(envMap.size(), 3);
168     assertEquals(envMap.get("GOOGLE_EXTERNAL_ACCOUNT_AUDIENCE"), credential.getAudience());
169     assertEquals(
170         envMap.get("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE"), credential.getSubjectTokenType());
171     assertEquals(envMap.get("GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE"), "0");
172     assertNull(envMap.get("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL"));
173     assertNull(envMap.get("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE"));
174   }
175 
176   @Test
refreshAccessToken_withoutServiceAccountImpersonation()177   public void refreshAccessToken_withoutServiceAccountImpersonation() throws IOException {
178     MockExternalAccountCredentialsTransportFactory transportFactory =
179         new MockExternalAccountCredentialsTransportFactory();
180 
181     transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime());
182 
183     PluggableAuthCredentials credential =
184         PluggableAuthCredentials.newBuilder(CREDENTIAL)
185             .setExecutableHandler(options -> "pluggableAuthToken")
186             .setTokenUrl(transportFactory.transport.getStsUrl())
187             .setHttpTransportFactory(transportFactory)
188             .build();
189 
190     AccessToken accessToken = credential.refreshAccessToken();
191 
192     assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());
193 
194     // Validate that the correct subject token was passed to STS.
195     Map<String, String> query =
196         TestUtils.parseQuery(transportFactory.transport.getRequests().get(0).getContentAsString());
197     assertEquals(query.get("subject_token"), "pluggableAuthToken");
198 
199     // Validate metrics header is set correctly on the sts request.
200     Map<String, List<String>> headers =
201         transportFactory.transport.getRequests().get(0).getHeaders();
202     ExternalAccountCredentialsTest.validateMetricsHeader(headers, "executable", false, false);
203   }
204 
205   @Test
refreshAccessToken_withServiceAccountImpersonation()206   public void refreshAccessToken_withServiceAccountImpersonation() throws IOException {
207     MockExternalAccountCredentialsTransportFactory transportFactory =
208         new MockExternalAccountCredentialsTransportFactory();
209 
210     transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime());
211 
212     PluggableAuthCredentials credential =
213         PluggableAuthCredentials.newBuilder()
214             .setAudience(
215                 "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider")
216             .setSubjectTokenType("subjectTokenType")
217             .setTokenInfoUrl("tokenInfoUrl")
218             .setTokenUrl(transportFactory.transport.getStsUrl())
219             .setCredentialSource(buildCredentialSource())
220             .setServiceAccountImpersonationUrl(
221                 transportFactory.transport.getServiceAccountImpersonationUrl())
222             .setHttpTransportFactory(transportFactory)
223             .build();
224 
225     credential =
226         PluggableAuthCredentials.newBuilder(credential)
227             .setExecutableHandler(options -> "pluggableAuthToken")
228             .build();
229 
230     AccessToken accessToken = credential.refreshAccessToken();
231 
232     assertEquals(
233         transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue());
234 
235     // Validate that the correct subject token was passed to STS.
236     Map<String, String> query =
237         TestUtils.parseQuery(transportFactory.transport.getRequests().get(0).getContentAsString());
238     assertEquals(query.get("subject_token"), "pluggableAuthToken");
239 
240     // Validate metrics header is set correctly on the sts request.
241     Map<String, List<String>> headers =
242         transportFactory.transport.getRequests().get(0).getHeaders();
243     ExternalAccountCredentialsTest.validateMetricsHeader(headers, "executable", true, false);
244   }
245 
246   @Test
refreshAccessToken_withServiceAccountImpersonationOptions()247   public void refreshAccessToken_withServiceAccountImpersonationOptions() throws IOException {
248     MockExternalAccountCredentialsTransportFactory transportFactory =
249         new MockExternalAccountCredentialsTransportFactory();
250 
251     transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime());
252 
253     PluggableAuthCredentials credential =
254         PluggableAuthCredentials.newBuilder()
255             .setAudience(
256                 "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider")
257             .setSubjectTokenType("subjectTokenType")
258             .setTokenInfoUrl("tokenInfoUrl")
259             .setTokenUrl(transportFactory.transport.getStsUrl())
260             .setCredentialSource(buildCredentialSource())
261             .setServiceAccountImpersonationUrl(
262                 transportFactory.transport.getServiceAccountImpersonationUrl())
263             .setServiceAccountImpersonationOptions(
264                 ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions(2800))
265             .setHttpTransportFactory(transportFactory)
266             .build();
267 
268     credential =
269         PluggableAuthCredentials.newBuilder(credential)
270             .setExecutableHandler(options -> "pluggableAuthToken")
271             .build();
272 
273     AccessToken accessToken = credential.refreshAccessToken();
274 
275     assertEquals(
276         transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue());
277 
278     // Validate that default lifetime was set correctly on the request.
279     GenericJson query =
280         OAuth2Utils.JSON_FACTORY
281             .createJsonParser(transportFactory.transport.getLastRequest().getContentAsString())
282             .parseAndClose(GenericJson.class);
283 
284     assertEquals("2800s", query.get("lifetime"));
285 
286     // Validate metrics header is set correctly on the sts request.
287     Map<String, List<String>> headers =
288         transportFactory.transport.getRequests().get(0).getHeaders();
289     ExternalAccountCredentialsTest.validateMetricsHeader(headers, "executable", true, true);
290   }
291 
292   @Test
pluggableAuthCredentialSource_allFields()293   public void pluggableAuthCredentialSource_allFields() {
294     Map<String, Object> source = new HashMap<>();
295     Map<String, Object> executable = new HashMap<>();
296     source.put("executable", executable);
297     executable.put("command", "/path/to/executable");
298     executable.put("timeout_millis", "10000");
299     executable.put("output_file", "/path/to/output/file");
300 
301     PluggableAuthCredentialSource credentialSource = new PluggableAuthCredentialSource(source);
302 
303     assertEquals(credentialSource.getCommand(), "/path/to/executable");
304     assertEquals(credentialSource.getTimeoutMs(), 10000);
305     assertEquals(credentialSource.getOutputFilePath(), "/path/to/output/file");
306   }
307 
308   @Test
pluggableAuthCredentialSource_noTimeoutProvided_setToDefault()309   public void pluggableAuthCredentialSource_noTimeoutProvided_setToDefault() {
310     Map<String, Object> source = new HashMap<>();
311     Map<String, Object> executable = new HashMap<>();
312     source.put("executable", executable);
313     executable.put("command", "command");
314     PluggableAuthCredentialSource credentialSource = new PluggableAuthCredentialSource(source);
315 
316     assertEquals(credentialSource.getCommand(), "command");
317     assertEquals(credentialSource.getTimeoutMs(), DEFAULT_EXECUTABLE_TIMEOUT_MS);
318     assertNull(credentialSource.getOutputFilePath());
319   }
320 
321   @Test
pluggableAuthCredentialSource_timeoutProvidedOutOfRange_throws()322   public void pluggableAuthCredentialSource_timeoutProvidedOutOfRange_throws() {
323     Map<String, Object> source = new HashMap<>();
324     Map<String, Object> executable = new HashMap<>();
325     source.put("executable", executable);
326 
327     executable.put("command", "command");
328 
329     int[] possibleOutOfRangeValues = new int[] {0, 4 * 1000, 121 * 1000};
330 
331     for (int value : possibleOutOfRangeValues) {
332       executable.put("timeout_millis", value);
333 
334       try {
335         new PluggableAuthCredentialSource(source);
336         fail("Should not be able to continue without exception.");
337       } catch (IllegalArgumentException exception) {
338         assertEquals(
339             String.format(
340                 "The executable timeout must be between %s and %s milliseconds.",
341                 MINIMUM_EXECUTABLE_TIMEOUT_MS, MAXIMUM_EXECUTABLE_TIMEOUT_MS),
342             exception.getMessage());
343       }
344     }
345   }
346 
347   @Test
pluggableAuthCredentialSource_validTimeoutProvided()348   public void pluggableAuthCredentialSource_validTimeoutProvided() {
349     Map<String, Object> source = new HashMap<>();
350     Map<String, Object> executable = new HashMap<>();
351     source.put("executable", executable);
352 
353     executable.put("command", "command");
354 
355     Object[] possibleValues = new Object[] {"10000", 10000, BigDecimal.valueOf(10000L)};
356 
357     for (Object value : possibleValues) {
358       executable.put("timeout_millis", value);
359       PluggableAuthCredentialSource credentialSource = new PluggableAuthCredentialSource(source);
360 
361       assertEquals(credentialSource.getCommand(), "command");
362       assertEquals(credentialSource.getTimeoutMs(), 10000);
363       assertNull(credentialSource.getOutputFilePath());
364     }
365   }
366 
367   @Test
pluggableAuthCredentialSource_missingExecutableField_throws()368   public void pluggableAuthCredentialSource_missingExecutableField_throws() {
369     try {
370       new PluggableAuthCredentialSource(new HashMap<>());
371       fail("Should not be able to continue without exception.");
372     } catch (IllegalArgumentException exception) {
373       assertEquals(
374           "Invalid credential source for PluggableAuth credentials.", exception.getMessage());
375     }
376   }
377 
378   @Test
pluggableAuthCredentialSource_missingExecutableCommandField_throws()379   public void pluggableAuthCredentialSource_missingExecutableCommandField_throws() {
380     Map<String, Object> source = new HashMap<>();
381     Map<String, Object> executable = new HashMap<>();
382     source.put("executable", executable);
383 
384     try {
385       new PluggableAuthCredentialSource(source);
386       fail("Should not be able to continue without exception.");
387     } catch (IllegalArgumentException exception) {
388       assertEquals(
389           "The PluggableAuthCredentialSource is missing the required 'command' field.",
390           exception.getMessage());
391     }
392   }
393 
394   @Test
builder_allFields()395   public void builder_allFields() throws IOException {
396     List<String> scopes = Arrays.asList("scope1", "scope2");
397 
398     PluggableAuthCredentialSource source = buildCredentialSource();
399     ExecutableHandler handler = options -> "Token";
400 
401     PluggableAuthCredentials credentials =
402         PluggableAuthCredentials.newBuilder()
403             .setExecutableHandler(handler)
404             .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
405             .setAudience("audience")
406             .setSubjectTokenType("subjectTokenType")
407             .setTokenUrl(STS_URL)
408             .setTokenInfoUrl("tokenInfoUrl")
409             .setCredentialSource(source)
410             .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
411             .setQuotaProjectId("quotaProjectId")
412             .setClientId("clientId")
413             .setClientSecret("clientSecret")
414             .setScopes(scopes)
415             .setUniverseDomain("universeDomain")
416             .build();
417 
418     assertEquals(handler, credentials.getExecutableHandler());
419     assertEquals("audience", credentials.getAudience());
420     assertEquals("subjectTokenType", credentials.getSubjectTokenType());
421     assertEquals(STS_URL, credentials.getTokenUrl());
422     assertEquals("tokenInfoUrl", credentials.getTokenInfoUrl());
423     assertEquals(
424         SERVICE_ACCOUNT_IMPERSONATION_URL, credentials.getServiceAccountImpersonationUrl());
425     assertEquals(source, credentials.getCredentialSource());
426     assertEquals("quotaProjectId", credentials.getQuotaProjectId());
427     assertEquals("clientId", credentials.getClientId());
428     assertEquals("clientSecret", credentials.getClientSecret());
429     assertEquals(scopes, credentials.getScopes());
430     assertEquals(SystemEnvironmentProvider.getInstance(), credentials.getEnvironmentProvider());
431     assertEquals("universeDomain", credentials.getUniverseDomain());
432   }
433 
434   @Test
builder_missingUniverseDomain_defaults()435   public void builder_missingUniverseDomain_defaults() throws IOException {
436     List<String> scopes = Arrays.asList("scope1", "scope2");
437 
438     PluggableAuthCredentialSource source = buildCredentialSource();
439     ExecutableHandler handler = options -> "Token";
440 
441     PluggableAuthCredentials credentials =
442         PluggableAuthCredentials.newBuilder()
443             .setExecutableHandler(handler)
444             .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
445             .setAudience("audience")
446             .setSubjectTokenType("subjectTokenType")
447             .setTokenUrl(STS_URL)
448             .setTokenInfoUrl("tokenInfoUrl")
449             .setCredentialSource(source)
450             .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
451             .setQuotaProjectId("quotaProjectId")
452             .setClientId("clientId")
453             .setClientSecret("clientSecret")
454             .setScopes(scopes)
455             .build();
456 
457     assertEquals(handler, credentials.getExecutableHandler());
458     assertEquals("audience", credentials.getAudience());
459     assertEquals("subjectTokenType", credentials.getSubjectTokenType());
460     assertEquals(STS_URL, credentials.getTokenUrl());
461     assertEquals("tokenInfoUrl", credentials.getTokenInfoUrl());
462     assertEquals(
463         SERVICE_ACCOUNT_IMPERSONATION_URL, credentials.getServiceAccountImpersonationUrl());
464     assertEquals(source, credentials.getCredentialSource());
465     assertEquals("quotaProjectId", credentials.getQuotaProjectId());
466     assertEquals("clientId", credentials.getClientId());
467     assertEquals("clientSecret", credentials.getClientSecret());
468     assertEquals(scopes, credentials.getScopes());
469     assertEquals(SystemEnvironmentProvider.getInstance(), credentials.getEnvironmentProvider());
470     assertEquals(GOOGLE_DEFAULT_UNIVERSE, credentials.getUniverseDomain());
471   }
472 
473   @Test
newBuilder_allFields()474   public void newBuilder_allFields() throws IOException {
475     List<String> scopes = Arrays.asList("scope1", "scope2");
476 
477     PluggableAuthCredentialSource source = buildCredentialSource();
478     ExecutableHandler handler = options -> "Token";
479 
480     PluggableAuthCredentials credentials =
481         PluggableAuthCredentials.newBuilder()
482             .setExecutableHandler(handler)
483             .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
484             .setAudience("audience")
485             .setSubjectTokenType("subjectTokenType")
486             .setTokenUrl(STS_URL)
487             .setTokenInfoUrl("tokenInfoUrl")
488             .setCredentialSource(source)
489             .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
490             .setQuotaProjectId("quotaProjectId")
491             .setClientId("clientId")
492             .setClientSecret("clientSecret")
493             .setScopes(scopes)
494             .setUniverseDomain("universeDomain")
495             .build();
496 
497     PluggableAuthCredentials newBuilderCreds =
498         PluggableAuthCredentials.newBuilder(credentials).build();
499     assertEquals(credentials.getAudience(), newBuilderCreds.getAudience());
500     assertEquals(credentials.getSubjectTokenType(), newBuilderCreds.getSubjectTokenType());
501     assertEquals(credentials.getTokenUrl(), newBuilderCreds.getTokenUrl());
502     assertEquals(credentials.getTokenInfoUrl(), newBuilderCreds.getTokenInfoUrl());
503     assertEquals(
504         credentials.getServiceAccountImpersonationUrl(),
505         newBuilderCreds.getServiceAccountImpersonationUrl());
506     assertEquals(credentials.getCredentialSource(), newBuilderCreds.getCredentialSource());
507     assertEquals(credentials.getQuotaProjectId(), newBuilderCreds.getQuotaProjectId());
508     assertEquals(credentials.getClientId(), newBuilderCreds.getClientId());
509     assertEquals(credentials.getClientSecret(), newBuilderCreds.getClientSecret());
510     assertEquals(credentials.getScopes(), newBuilderCreds.getScopes());
511     assertEquals(credentials.getEnvironmentProvider(), newBuilderCreds.getEnvironmentProvider());
512     assertEquals(credentials.getUniverseDomain(), newBuilderCreds.getUniverseDomain());
513   }
514 
515   @Test
newBuilder_noUniverseDomain_defaults()516   public void newBuilder_noUniverseDomain_defaults() throws IOException {
517     List<String> scopes = Arrays.asList("scope1", "scope2");
518 
519     PluggableAuthCredentialSource source = buildCredentialSource();
520     ExecutableHandler handler = options -> "Token";
521 
522     PluggableAuthCredentials credentials =
523         PluggableAuthCredentials.newBuilder()
524             .setExecutableHandler(handler)
525             .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
526             .setAudience("audience")
527             .setSubjectTokenType("subjectTokenType")
528             .setTokenUrl(STS_URL)
529             .setTokenInfoUrl("tokenInfoUrl")
530             .setCredentialSource(source)
531             .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
532             .setQuotaProjectId("quotaProjectId")
533             .setClientId("clientId")
534             .setClientSecret("clientSecret")
535             .setScopes(scopes)
536             .build();
537 
538     PluggableAuthCredentials newBuilderCreds =
539         PluggableAuthCredentials.newBuilder(credentials).build();
540     assertEquals(credentials.getAudience(), newBuilderCreds.getAudience());
541     assertEquals(credentials.getSubjectTokenType(), newBuilderCreds.getSubjectTokenType());
542     assertEquals(credentials.getTokenUrl(), newBuilderCreds.getTokenUrl());
543     assertEquals(credentials.getTokenInfoUrl(), newBuilderCreds.getTokenInfoUrl());
544     assertEquals(
545         credentials.getServiceAccountImpersonationUrl(),
546         newBuilderCreds.getServiceAccountImpersonationUrl());
547     assertEquals(credentials.getCredentialSource(), newBuilderCreds.getCredentialSource());
548     assertEquals(credentials.getQuotaProjectId(), newBuilderCreds.getQuotaProjectId());
549     assertEquals(credentials.getClientId(), newBuilderCreds.getClientId());
550     assertEquals(credentials.getClientSecret(), newBuilderCreds.getClientSecret());
551     assertEquals(credentials.getScopes(), newBuilderCreds.getScopes());
552     assertEquals(credentials.getEnvironmentProvider(), newBuilderCreds.getEnvironmentProvider());
553     assertEquals(GOOGLE_DEFAULT_UNIVERSE, newBuilderCreds.getUniverseDomain());
554   }
555 
556   @Test
createdScoped_clonedCredentialWithAddedScopes()557   public void createdScoped_clonedCredentialWithAddedScopes() throws IOException {
558     PluggableAuthCredentials credentials =
559         PluggableAuthCredentials.newBuilder(CREDENTIAL)
560             .setExecutableHandler(options -> "pluggableAuthToken")
561             .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
562             .setQuotaProjectId("quotaProjectId")
563             .setClientId("clientId")
564             .setClientSecret("clientSecret")
565             .setUniverseDomain("universeDomain")
566             .build();
567 
568     List<String> newScopes = Arrays.asList("scope1", "scope2");
569 
570     PluggableAuthCredentials newCredentials = credentials.createScoped(newScopes);
571 
572     assertEquals(credentials.getAudience(), newCredentials.getAudience());
573     assertEquals(credentials.getSubjectTokenType(), newCredentials.getSubjectTokenType());
574     assertEquals(credentials.getTokenUrl(), newCredentials.getTokenUrl());
575     assertEquals(credentials.getTokenInfoUrl(), newCredentials.getTokenInfoUrl());
576     assertEquals(
577         credentials.getServiceAccountImpersonationUrl(),
578         newCredentials.getServiceAccountImpersonationUrl());
579     assertEquals(credentials.getCredentialSource(), newCredentials.getCredentialSource());
580     assertEquals(newScopes, newCredentials.getScopes());
581     assertEquals(credentials.getQuotaProjectId(), newCredentials.getQuotaProjectId());
582     assertEquals(credentials.getClientId(), newCredentials.getClientId());
583     assertEquals(credentials.getClientSecret(), newCredentials.getClientSecret());
584     assertEquals(credentials.getExecutableHandler(), newCredentials.getExecutableHandler());
585     assertEquals(credentials.getUniverseDomain(), newCredentials.getUniverseDomain());
586     assertEquals("universeDomain", newCredentials.getUniverseDomain());
587   }
588 
589   @Test
serialize()590   public void serialize() throws IOException, ClassNotFoundException {
591     PluggableAuthCredentials testCredentials =
592         PluggableAuthCredentials.newBuilder(CREDENTIAL)
593             .setExecutableHandler(options -> "pluggableAuthToken")
594             .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
595             .setQuotaProjectId("quotaProjectId")
596             .setClientId("clientId")
597             .setClientSecret("clientSecret")
598             .setUniverseDomain("universeDomain")
599             .build();
600 
601     // PluggableAuthCredentials are not serializable
602     assertThrows(NotSerializableException.class, () -> serializeAndDeserialize(testCredentials));
603   }
604 
buildCredentialSource()605   private static PluggableAuthCredentialSource buildCredentialSource() {
606     return buildCredentialSource("command", null, null);
607   }
608 
buildCredentialSource( String command, @Nullable String timeoutMs, @Nullable String outputFile)609   private static PluggableAuthCredentialSource buildCredentialSource(
610       String command, @Nullable String timeoutMs, @Nullable String outputFile) {
611     Map<String, Object> source = new HashMap<>();
612     Map<String, Object> executable = new HashMap<>();
613     source.put("executable", executable);
614     executable.put("command", command);
615     if (timeoutMs != null) {
616       executable.put("timeout_millis", timeoutMs);
617     }
618     if (outputFile != null) {
619       executable.put("output_file", outputFile);
620     }
621 
622     return new PluggableAuthCredentialSource(source);
623   }
624 
writeCredentialsStream(String tokenUrl)625   static InputStream writeCredentialsStream(String tokenUrl) throws IOException {
626     GenericJson json = new GenericJson();
627     json.put("audience", "audience");
628     json.put("subject_token_type", "subjectTokenType");
629     json.put("token_url", tokenUrl);
630     json.put("token_info_url", "tokenInfoUrl");
631     json.put("type", ExternalAccountCredentials.EXTERNAL_ACCOUNT_FILE_TYPE);
632 
633     GenericJson credentialSource = new GenericJson();
634     GenericJson executable = new GenericJson();
635     executable.put("command", "/path/to/executable");
636     credentialSource.put("executable", executable);
637 
638     json.put("credential_source", credentialSource);
639     return TestUtils.jsonToInputStream(json);
640   }
641 }
642