• 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.oauth2.MockExternalAccountCredentialsTransport.SERVICE_ACCOUNT_IMPERSONATION_URL;
35 import static org.junit.Assert.assertEquals;
36 import static org.junit.Assert.assertNotNull;
37 import static org.junit.Assert.assertNull;
38 import static org.junit.Assert.assertSame;
39 import static org.junit.Assert.assertTrue;
40 import static org.junit.Assert.fail;
41 
42 import com.google.api.client.http.HttpTransport;
43 import com.google.api.client.json.GenericJson;
44 import com.google.api.client.util.Clock;
45 import com.google.auth.TestUtils;
46 import com.google.auth.http.HttpTransportFactory;
47 import com.google.auth.oauth2.ExternalAccountCredentials.SubjectTokenTypes;
48 import com.google.auth.oauth2.ExternalAccountCredentialsTest.TestExternalAccountCredentials.TestCredentialSource;
49 import java.io.ByteArrayInputStream;
50 import java.io.IOException;
51 import java.math.BigDecimal;
52 import java.net.URI;
53 import java.util.Arrays;
54 import java.util.Date;
55 import java.util.HashMap;
56 import java.util.List;
57 import java.util.Locale;
58 import java.util.Map;
59 import org.junit.Before;
60 import org.junit.Test;
61 import org.junit.runner.RunWith;
62 import org.junit.runners.JUnit4;
63 
64 /** Tests for {@link ExternalAccountCredentials}. */
65 @RunWith(JUnit4.class)
66 public class ExternalAccountCredentialsTest extends BaseSerializationTest {
67 
68   private static final String STS_URL = "https://sts.googleapis.com/v1/token";
69   private static final String GOOGLE_DEFAULT_UNIVERSE = "googleapis.com";
70 
71   private static final Map<String, Object> FILE_CREDENTIAL_SOURCE_MAP =
72       new HashMap<String, Object>() {
73         {
74           put("file", "file");
75         }
76       };
77 
78   static class MockExternalAccountCredentialsTransportFactory implements HttpTransportFactory {
79 
80     MockExternalAccountCredentialsTransport transport =
81         new MockExternalAccountCredentialsTransport();
82 
83     @Override
create()84     public HttpTransport create() {
85       return transport;
86     }
87   }
88 
89   private MockExternalAccountCredentialsTransportFactory transportFactory;
90 
91   @Before
setup()92   public void setup() {
93     transportFactory = new MockExternalAccountCredentialsTransportFactory();
94   }
95 
96   @Test
fromStream_identityPoolCredentials()97   public void fromStream_identityPoolCredentials() throws IOException {
98     GenericJson json = buildJsonIdentityPoolCredential();
99 
100     ExternalAccountCredentials credential =
101         ExternalAccountCredentials.fromStream(TestUtils.jsonToInputStream(json));
102 
103     assertTrue(credential instanceof IdentityPoolCredentials);
104   }
105 
106   @Test
fromStream_awsCredentials()107   public void fromStream_awsCredentials() throws IOException {
108     GenericJson json = buildJsonAwsCredential();
109 
110     ExternalAccountCredentials credential =
111         ExternalAccountCredentials.fromStream(TestUtils.jsonToInputStream(json));
112 
113     assertTrue(credential instanceof AwsCredentials);
114   }
115 
116   @Test
fromStream_pluggableAuthCredentials()117   public void fromStream_pluggableAuthCredentials() throws IOException {
118     GenericJson json = buildJsonPluggableAuthCredential();
119 
120     ExternalAccountCredentials credential =
121         ExternalAccountCredentials.fromStream(TestUtils.jsonToInputStream(json));
122 
123     assertTrue(credential instanceof PluggableAuthCredentials);
124   }
125 
126   @Test
fromStream_invalidStream_throws()127   public void fromStream_invalidStream_throws() throws IOException {
128     GenericJson json = buildJsonAwsCredential();
129 
130     json.put("audience", new HashMap<>());
131 
132     try {
133       ExternalAccountCredentials.fromStream(TestUtils.jsonToInputStream(json));
134       fail("Should fail.");
135     } catch (CredentialFormatException e) {
136       assertEquals("An invalid input stream was provided.", e.getMessage());
137     }
138   }
139 
140   @Test
fromStream_nullTransport_throws()141   public void fromStream_nullTransport_throws() throws IOException {
142     try {
143       ExternalAccountCredentials.fromStream(
144           new ByteArrayInputStream("foo".getBytes()), /* transportFactory= */ null);
145       fail("NullPointerException should be thrown.");
146     } catch (NullPointerException e) {
147       // Expected.
148     }
149   }
150 
151   @Test
fromStream_nullStream_throws()152   public void fromStream_nullStream_throws() throws IOException {
153     try {
154       ExternalAccountCredentials.fromStream(
155           /* credentialsStream= */ null, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
156       fail("NullPointerException should be thrown.");
157     } catch (NullPointerException e) {
158       // Expected.
159     }
160   }
161 
162   @Test
fromStream_invalidWorkloadAudience_throws()163   public void fromStream_invalidWorkloadAudience_throws() throws IOException {
164     try {
165       GenericJson json = buildJsonIdentityPoolWorkforceCredential();
166       json.put("audience", "invalidAudience");
167       ExternalAccountCredentials credential =
168           ExternalAccountCredentials.fromStream(TestUtils.jsonToInputStream(json));
169       fail("CredentialFormatException should be thrown.");
170     } catch (CredentialFormatException e) {
171       assertEquals("An invalid input stream was provided.", e.getMessage());
172     }
173   }
174 
175   @Test
fromJson_identityPoolCredentialsWorkload()176   public void fromJson_identityPoolCredentialsWorkload() throws IOException {
177     ExternalAccountCredentials credential =
178         ExternalAccountCredentials.fromJson(
179             buildJsonIdentityPoolCredential(), OAuth2Utils.HTTP_TRANSPORT_FACTORY);
180 
181     assertTrue(credential instanceof IdentityPoolCredentials);
182     assertEquals(
183         "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider",
184         credential.getAudience());
185     assertEquals("subjectTokenType", credential.getSubjectTokenType());
186     assertEquals(STS_URL, credential.getTokenUrl());
187     assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
188     assertNotNull(credential.getCredentialSource());
189     assertEquals(GOOGLE_DEFAULT_UNIVERSE, credential.getUniverseDomain());
190   }
191 
192   @Test
fromJson_identityPoolCredentialsWorkforce()193   public void fromJson_identityPoolCredentialsWorkforce() throws IOException {
194     ExternalAccountCredentials credential =
195         ExternalAccountCredentials.fromJson(
196             buildJsonIdentityPoolWorkforceCredential(), OAuth2Utils.HTTP_TRANSPORT_FACTORY);
197 
198     assertTrue(credential instanceof IdentityPoolCredentials);
199     assertEquals(
200         "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider",
201         credential.getAudience());
202     assertEquals("subjectTokenType", credential.getSubjectTokenType());
203     assertEquals(STS_URL, credential.getTokenUrl());
204     assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
205     assertEquals("userProject", credential.getWorkforcePoolUserProject());
206     assertNotNull(credential.getCredentialSource());
207     assertEquals(GOOGLE_DEFAULT_UNIVERSE, credential.getUniverseDomain());
208   }
209 
210   @Test
fromJson_identityPoolCredentialsWithServiceAccountImpersonationOptions()211   public void fromJson_identityPoolCredentialsWithServiceAccountImpersonationOptions()
212       throws IOException {
213     GenericJson identityPoolCredentialJson = buildJsonIdentityPoolCredential();
214     identityPoolCredentialJson.set(
215         "service_account_impersonation", buildServiceAccountImpersonationOptions(2800));
216 
217     ExternalAccountCredentials credential =
218         ExternalAccountCredentials.fromJson(
219             identityPoolCredentialJson, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
220 
221     assertTrue(credential instanceof IdentityPoolCredentials);
222     assertEquals(
223         "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider",
224         credential.getAudience());
225     assertEquals("subjectTokenType", credential.getSubjectTokenType());
226     assertEquals(STS_URL, credential.getTokenUrl());
227     assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
228     assertNotNull(credential.getCredentialSource());
229     assertEquals(2800, credential.getServiceAccountImpersonationOptions().getLifetime());
230     assertEquals(GOOGLE_DEFAULT_UNIVERSE, credential.getUniverseDomain());
231   }
232 
233   @Test
fromJson_identityPoolCredentialsWithUniverseDomain()234   public void fromJson_identityPoolCredentialsWithUniverseDomain() throws IOException {
235     GenericJson identityPoolCredentialJson = buildJsonIdentityPoolCredential();
236     identityPoolCredentialJson.set("universe_domain", "universeDomain");
237 
238     ExternalAccountCredentials credential =
239         ExternalAccountCredentials.fromJson(
240             identityPoolCredentialJson, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
241 
242     assertTrue(credential instanceof IdentityPoolCredentials);
243     assertNotNull(credential.getCredentialSource());
244     assertEquals(
245         "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider",
246         credential.getAudience());
247     assertEquals("subjectTokenType", credential.getSubjectTokenType());
248     assertEquals(STS_URL, credential.getTokenUrl());
249     assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
250     assertEquals("universeDomain", credential.getUniverseDomain());
251   }
252 
253   @Test
fromJson_awsCredentials()254   public void fromJson_awsCredentials() throws IOException {
255     ExternalAccountCredentials credential =
256         ExternalAccountCredentials.fromJson(
257             buildJsonAwsCredential(), OAuth2Utils.HTTP_TRANSPORT_FACTORY);
258 
259     assertTrue(credential instanceof AwsCredentials);
260     assertEquals("audience", credential.getAudience());
261     assertEquals("subjectTokenType", credential.getSubjectTokenType());
262     assertEquals(STS_URL, credential.getTokenUrl());
263     assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
264     assertNotNull(credential.getCredentialSource());
265     assertEquals(GOOGLE_DEFAULT_UNIVERSE, credential.getUniverseDomain());
266   }
267 
268   @Test
fromJson_awsCredentialsWithServiceAccountImpersonationOptions()269   public void fromJson_awsCredentialsWithServiceAccountImpersonationOptions() throws IOException {
270     GenericJson awsCredentialJson = buildJsonAwsCredential();
271     awsCredentialJson.set(
272         "service_account_impersonation", buildServiceAccountImpersonationOptions(2800));
273 
274     ExternalAccountCredentials credential =
275         ExternalAccountCredentials.fromJson(awsCredentialJson, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
276 
277     assertTrue(credential instanceof AwsCredentials);
278     assertEquals("audience", credential.getAudience());
279     assertEquals("subjectTokenType", credential.getSubjectTokenType());
280     assertEquals(STS_URL, credential.getTokenUrl());
281     assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
282     assertNotNull(credential.getCredentialSource());
283     assertEquals(2800, credential.getServiceAccountImpersonationOptions().getLifetime());
284     assertEquals(GOOGLE_DEFAULT_UNIVERSE, credential.getUniverseDomain());
285   }
286 
287   @Test
fromJson_awsCredentialsWithUniverseDomain()288   public void fromJson_awsCredentialsWithUniverseDomain() throws IOException {
289     GenericJson awsCredentialJson = buildJsonAwsCredential();
290     awsCredentialJson.set("universe_domain", "universeDomain");
291 
292     ExternalAccountCredentials credential =
293         ExternalAccountCredentials.fromJson(awsCredentialJson, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
294 
295     assertTrue(credential instanceof AwsCredentials);
296     assertEquals("audience", credential.getAudience());
297     assertEquals("subjectTokenType", credential.getSubjectTokenType());
298     assertEquals(STS_URL, credential.getTokenUrl());
299     assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
300     assertEquals("universeDomain", credential.getUniverseDomain());
301     assertNotNull(credential.getCredentialSource());
302   }
303 
304   @Test
fromJson_pluggableAuthCredentials()305   public void fromJson_pluggableAuthCredentials() throws IOException {
306     ExternalAccountCredentials credential =
307         ExternalAccountCredentials.fromJson(
308             buildJsonPluggableAuthCredential(), OAuth2Utils.HTTP_TRANSPORT_FACTORY);
309 
310     assertTrue(credential instanceof PluggableAuthCredentials);
311     assertEquals("audience", credential.getAudience());
312     assertEquals("subjectTokenType", credential.getSubjectTokenType());
313     assertEquals(STS_URL, credential.getTokenUrl());
314     assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
315     assertNotNull(credential.getCredentialSource());
316 
317     PluggableAuthCredentialSource source =
318         (PluggableAuthCredentialSource) credential.getCredentialSource();
319     assertEquals("command", source.getCommand());
320     assertEquals(30000, source.getTimeoutMs()); // Default timeout is 30s.
321     assertNull(source.getOutputFilePath());
322     assertEquals(GOOGLE_DEFAULT_UNIVERSE, credential.getUniverseDomain());
323   }
324 
325   @Test
fromJson_pluggableAuthCredentialsWorkforce()326   public void fromJson_pluggableAuthCredentialsWorkforce() throws IOException {
327     ExternalAccountCredentials credential =
328         ExternalAccountCredentials.fromJson(
329             buildJsonPluggableAuthWorkforceCredential(), OAuth2Utils.HTTP_TRANSPORT_FACTORY);
330 
331     assertTrue(credential instanceof PluggableAuthCredentials);
332     assertEquals(
333         "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider",
334         credential.getAudience());
335     assertEquals("subjectTokenType", credential.getSubjectTokenType());
336     assertEquals(STS_URL, credential.getTokenUrl());
337     assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
338     assertEquals("userProject", credential.getWorkforcePoolUserProject());
339 
340     assertNotNull(credential.getCredentialSource());
341 
342     PluggableAuthCredentialSource source =
343         (PluggableAuthCredentialSource) credential.getCredentialSource();
344     assertEquals("command", source.getCommand());
345     assertEquals(30000, source.getTimeoutMs()); // Default timeout is 30s.
346     assertNull(source.getOutputFilePath());
347     assertEquals(GOOGLE_DEFAULT_UNIVERSE, credential.getUniverseDomain());
348   }
349 
350   @Test
351   @SuppressWarnings("unchecked")
fromJson_pluggableAuthCredentials_allExecutableOptionsSet()352   public void fromJson_pluggableAuthCredentials_allExecutableOptionsSet() throws IOException {
353     GenericJson json = buildJsonPluggableAuthCredential();
354     Map<String, Object> credentialSourceMap = (Map<String, Object>) json.get("credential_source");
355     // Add optional params to the executable config (timeout, output file path).
356     Map<String, Object> executableConfig =
357         (Map<String, Object>) credentialSourceMap.get("executable");
358     executableConfig.put("timeout_millis", 5000);
359     executableConfig.put("output_file", "path/to/output/file");
360 
361     ExternalAccountCredentials credential =
362         ExternalAccountCredentials.fromJson(json, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
363 
364     assertTrue(credential instanceof PluggableAuthCredentials);
365     assertEquals("audience", credential.getAudience());
366     assertEquals("subjectTokenType", credential.getSubjectTokenType());
367     assertEquals(STS_URL, credential.getTokenUrl());
368     assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
369     assertNotNull(credential.getCredentialSource());
370 
371     PluggableAuthCredentialSource source =
372         (PluggableAuthCredentialSource) credential.getCredentialSource();
373     assertEquals("command", source.getCommand());
374     assertEquals("path/to/output/file", source.getOutputFilePath());
375     assertEquals(5000, source.getTimeoutMs());
376     assertEquals(GOOGLE_DEFAULT_UNIVERSE, credential.getUniverseDomain());
377   }
378 
379   @Test
fromJson_pluggableAuthCredentialsWithServiceAccountImpersonationOptions()380   public void fromJson_pluggableAuthCredentialsWithServiceAccountImpersonationOptions()
381       throws IOException {
382     GenericJson pluggableAuthCredentialJson = buildJsonPluggableAuthCredential();
383     pluggableAuthCredentialJson.set(
384         "service_account_impersonation", buildServiceAccountImpersonationOptions(2800));
385 
386     ExternalAccountCredentials credential =
387         ExternalAccountCredentials.fromJson(
388             pluggableAuthCredentialJson, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
389 
390     assertTrue(credential instanceof PluggableAuthCredentials);
391     assertEquals("audience", credential.getAudience());
392     assertEquals("subjectTokenType", credential.getSubjectTokenType());
393     assertEquals(STS_URL, credential.getTokenUrl());
394     assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
395     assertNotNull(credential.getCredentialSource());
396     assertEquals(2800, credential.getServiceAccountImpersonationOptions().getLifetime());
397 
398     PluggableAuthCredentialSource source =
399         (PluggableAuthCredentialSource) credential.getCredentialSource();
400     assertEquals("command", source.getCommand());
401     assertEquals(30000, source.getTimeoutMs()); // Default timeout is 30s.
402     assertNull(source.getOutputFilePath());
403     assertEquals(GOOGLE_DEFAULT_UNIVERSE, credential.getUniverseDomain());
404   }
405 
406   @Test
407   @SuppressWarnings("unchecked")
fromJson_pluggableAuthCredentials_withUniverseDomain()408   public void fromJson_pluggableAuthCredentials_withUniverseDomain() throws IOException {
409     GenericJson json = buildJsonPluggableAuthCredential();
410     json.set("universe_domain", "universeDomain");
411 
412     Map<String, Object> credentialSourceMap = (Map<String, Object>) json.get("credential_source");
413     // Add optional params to the executable config (timeout, output file path).
414     Map<String, Object> executableConfig =
415         (Map<String, Object>) credentialSourceMap.get("executable");
416     executableConfig.put("timeout_millis", 5000);
417     executableConfig.put("output_file", "path/to/output/file");
418 
419     ExternalAccountCredentials credential =
420         ExternalAccountCredentials.fromJson(json, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
421 
422     assertTrue(credential instanceof PluggableAuthCredentials);
423     assertEquals("audience", credential.getAudience());
424     assertEquals("subjectTokenType", credential.getSubjectTokenType());
425     assertEquals(STS_URL, credential.getTokenUrl());
426     assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
427     assertNotNull(credential.getCredentialSource());
428 
429     PluggableAuthCredentialSource source =
430         (PluggableAuthCredentialSource) credential.getCredentialSource();
431     assertEquals("command", source.getCommand());
432     assertEquals("path/to/output/file", source.getOutputFilePath());
433     assertEquals(5000, source.getTimeoutMs());
434     assertEquals("universeDomain", credential.getUniverseDomain());
435   }
436 
437   @Test
fromJson_pluggableAuthCredentialsWithUniverseDomain()438   public void fromJson_pluggableAuthCredentialsWithUniverseDomain() throws IOException {
439     GenericJson pluggableAuthCredentialJson = buildJsonPluggableAuthCredential();
440     pluggableAuthCredentialJson.set("universe_domain", "universeDomain");
441 
442     ExternalAccountCredentials credential =
443         ExternalAccountCredentials.fromJson(
444             pluggableAuthCredentialJson, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
445 
446     assertTrue(credential instanceof PluggableAuthCredentials);
447     assertEquals("audience", credential.getAudience());
448     assertEquals("subjectTokenType", credential.getSubjectTokenType());
449     assertEquals(STS_URL, credential.getTokenUrl());
450     assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
451     assertNotNull(credential.getCredentialSource());
452     assertEquals("universeDomain", credential.getUniverseDomain());
453 
454     PluggableAuthCredentialSource source =
455         (PluggableAuthCredentialSource) credential.getCredentialSource();
456     assertEquals("command", source.getCommand());
457     assertEquals(30000, source.getTimeoutMs()); // Default timeout is 30s.
458     assertNull(source.getOutputFilePath());
459   }
460 
461   @Test
fromJson_nullJson_throws()462   public void fromJson_nullJson_throws() throws IOException {
463     try {
464       ExternalAccountCredentials.fromJson(/* json= */ null, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
465       fail("Exception should be thrown.");
466     } catch (NullPointerException e) {
467       // Expected.
468     }
469   }
470 
471   @Test
fromJson_nullTransport_throws()472   public void fromJson_nullTransport_throws() throws IOException {
473     try {
474       ExternalAccountCredentials.fromJson(
475           new HashMap<String, Object>(), /* transportFactory= */ null);
476       fail("Exception should be thrown.");
477     } catch (NullPointerException e) {
478       // Expected.
479     }
480   }
481 
482   @Test
fromJson_invalidWorkforceAudiences_throws()483   public void fromJson_invalidWorkforceAudiences_throws() throws IOException {
484     List<String> invalidAudiences =
485         Arrays.asList(
486             "//iam.googleapis.com/locations/global/workloadIdentityPools/pool/providers/provider",
487             "//iam.googleapis.com/locations/global/workforcepools/pool/providers/provider",
488             "//iam.googleapis.com/locations/global/workforcePools/providers/provider",
489             "//iam.googleapis.com/locations/global/workforcePools/providers",
490             "//iam.googleapis.com/locations/global/workforcePools/",
491             "//iam.googleapis.com/locations//workforcePools/providers",
492             "//iam.googleapis.com/notlocations/global/workforcePools/providers",
493             "//iam.googleapis.com/locations/global/workforce/providers");
494 
495     for (String audience : invalidAudiences) {
496       try {
497         GenericJson json = buildJsonIdentityPoolCredential();
498         json.put("audience", audience);
499         json.put("workforce_pool_user_project", "userProject");
500 
501         ExternalAccountCredentials.fromJson(json, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
502         fail("Exception should be thrown.");
503       } catch (IllegalArgumentException e) {
504         assertEquals(
505             "The workforce_pool_user_project parameter should only be provided for a Workforce Pool configuration.",
506             e.getMessage());
507       }
508     }
509   }
510 
511   @Test
constructor_builder()512   public void constructor_builder() throws IOException {
513     HashMap<String, Object> credentialSource = new HashMap<>();
514     credentialSource.put("file", "file");
515 
516     ExternalAccountCredentials credentials =
517         IdentityPoolCredentials.newBuilder()
518             .setHttpTransportFactory(transportFactory)
519             .setAudience(
520                 "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider")
521             .setSubjectTokenType("subjectTokenType")
522             .setTokenUrl(STS_URL)
523             .setTokenInfoUrl("https://tokeninfo.com")
524             .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
525             .setCredentialSource(new TestCredentialSource(credentialSource))
526             .setScopes(Arrays.asList("scope1", "scope2"))
527             .setQuotaProjectId("projectId")
528             .setClientId("clientId")
529             .setClientSecret("clientSecret")
530             .setWorkforcePoolUserProject("workforcePoolUserProject")
531             .setUniverseDomain("universeDomain")
532             .build();
533 
534     assertEquals(
535         "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider",
536         credentials.getAudience());
537     assertEquals("subjectTokenType", credentials.getSubjectTokenType());
538     assertEquals(STS_URL, credentials.getTokenUrl());
539     assertEquals("https://tokeninfo.com", credentials.getTokenInfoUrl());
540     assertEquals(
541         SERVICE_ACCOUNT_IMPERSONATION_URL, credentials.getServiceAccountImpersonationUrl());
542     assertEquals(Arrays.asList("scope1", "scope2"), credentials.getScopes());
543     assertEquals("projectId", credentials.getQuotaProjectId());
544     assertEquals("clientId", credentials.getClientId());
545     assertEquals("clientSecret", credentials.getClientSecret());
546     assertEquals("workforcePoolUserProject", credentials.getWorkforcePoolUserProject());
547     assertEquals("universeDomain", credentials.getUniverseDomain());
548     assertNotNull(credentials.getCredentialSource());
549   }
550 
551   @Test
constructor_builder_defaultTokenUrl()552   public void constructor_builder_defaultTokenUrl() {
553     HashMap<String, Object> credentialSource = new HashMap<>();
554     credentialSource.put("file", "file");
555 
556     ExternalAccountCredentials credentials =
557         IdentityPoolCredentials.newBuilder()
558             .setHttpTransportFactory(transportFactory)
559             .setAudience(
560                 "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider")
561             .setSubjectTokenType("subjectTokenType")
562             .setCredentialSource(new TestCredentialSource(credentialSource))
563             .build();
564 
565     assertEquals(STS_URL, credentials.getTokenUrl());
566   }
567 
568   @Test
constructor_builder_subjectTokenTypeEnum()569   public void constructor_builder_subjectTokenTypeEnum() {
570     HashMap<String, Object> credentialSource = new HashMap<>();
571     credentialSource.put("file", "file");
572 
573     ExternalAccountCredentials credentials =
574         IdentityPoolCredentials.newBuilder()
575             .setHttpTransportFactory(transportFactory)
576             .setAudience(
577                 "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider")
578             .setSubjectTokenType(SubjectTokenTypes.SAML2)
579             .setTokenUrl(STS_URL)
580             .setCredentialSource(new TestCredentialSource(credentialSource))
581             .build();
582 
583     assertEquals(SubjectTokenTypes.SAML2.value, credentials.getSubjectTokenType());
584   }
585 
586   @Test
constructor_builder_invalidTokenUrl()587   public void constructor_builder_invalidTokenUrl() {
588     try {
589       ExternalAccountCredentials.Builder builder =
590           TestExternalAccountCredentials.newBuilder()
591               .setHttpTransportFactory(transportFactory)
592               .setAudience("audience")
593               .setSubjectTokenType("subjectTokenType")
594               .setTokenUrl("tokenUrl")
595               .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP));
596       new TestExternalAccountCredentials(builder);
597       fail("Should not be able to continue without exception.");
598     } catch (IllegalArgumentException exception) {
599       assertEquals("The provided token URL is invalid.", exception.getMessage());
600     }
601   }
602 
603   @Test
constructor_builder_invalidServiceAccountImpersonationUrl()604   public void constructor_builder_invalidServiceAccountImpersonationUrl() {
605     try {
606       ExternalAccountCredentials.Builder builder =
607           TestExternalAccountCredentials.newBuilder()
608               .setHttpTransportFactory(transportFactory)
609               .setAudience("audience")
610               .setSubjectTokenType("subjectTokenType")
611               .setTokenUrl("tokenUrl")
612               .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP))
613               .setServiceAccountImpersonationUrl("serviceAccountImpersonationUrl");
614       new TestExternalAccountCredentials(builder);
615       fail("Should not be able to continue without exception.");
616     } catch (IllegalArgumentException exception) {
617       assertEquals("The provided token URL is invalid.", exception.getMessage());
618     }
619   }
620 
621   @Test
constructor_builderWithInvalidWorkforceAudiences_throws()622   public void constructor_builderWithInvalidWorkforceAudiences_throws() {
623     List<String> invalidAudiences =
624         Arrays.asList(
625             "",
626             "//iam.googleapis.com/projects/x23/locations/global/workloadIdentityPools/pool/providers/provider",
627             "//iam.googleapis.com/locations/global/workforcepools/pool/providers/provider",
628             "//iam.googleapis.com/locations/global/workforcePools/providers/provider",
629             "//iam.googleapis.com/locations/global/workforcePools/providers",
630             "//iam.googleapis.com/locations/global/workforcePools/",
631             "//iam.googleapis.com/locations//workforcePools/providers",
632             "//iam.googleapis.com/notlocations/global/workforcePools/providers",
633             "//iam.googleapis.com/locations/global/workforce/providers");
634 
635     HashMap<String, Object> credentialSource = new HashMap<>();
636     credentialSource.put("file", "file");
637     for (String audience : invalidAudiences) {
638       try {
639         TestExternalAccountCredentials.newBuilder()
640             .setWorkforcePoolUserProject("workforcePoolUserProject")
641             .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
642             .setAudience(audience)
643             .setSubjectTokenType("subjectTokenType")
644             .setTokenUrl(STS_URL)
645             .setCredentialSource(new TestCredentialSource(credentialSource))
646             .build();
647         fail("Should not be able to continue without exception.");
648       } catch (IllegalArgumentException exception) {
649         assertEquals(
650             "The workforce_pool_user_project parameter should only be provided for a Workforce Pool configuration.",
651             exception.getMessage());
652       }
653     }
654   }
655 
656   @Test
constructor_builderWithEmptyWorkforceUserProjectAndWorkforceAudience()657   public void constructor_builderWithEmptyWorkforceUserProjectAndWorkforceAudience() {
658     HashMap<String, Object> credentialSource = new HashMap<>();
659     credentialSource.put("file", "file");
660     // No exception should be thrown.
661     TestExternalAccountCredentials.newBuilder()
662         .setWorkforcePoolUserProject("")
663         .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
664         .setAudience("//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider")
665         .setSubjectTokenType("subjectTokenType")
666         .setTokenUrl(STS_URL)
667         .setCredentialSource(new TestCredentialSource(credentialSource))
668         .build();
669   }
670 
671   @Test
constructor_builder_invalidTokenLifetime_throws()672   public void constructor_builder_invalidTokenLifetime_throws() {
673     Map<String, Object> invalidOptionsMap = new HashMap<String, Object>();
674     invalidOptionsMap.put("token_lifetime_seconds", "thisIsAString");
675 
676     try {
677       IdentityPoolCredentials.newBuilder()
678           .setHttpTransportFactory(transportFactory)
679           .setAudience(
680               "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider")
681           .setSubjectTokenType("subjectTokenType")
682           .setTokenUrl(STS_URL)
683           .setTokenInfoUrl("https://tokeninfo.com")
684           .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
685           .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP))
686           .setScopes(Arrays.asList("scope1", "scope2"))
687           .setQuotaProjectId("projectId")
688           .setClientId("clientId")
689           .setClientSecret("clientSecret")
690           .setWorkforcePoolUserProject("workforcePoolUserProject")
691           .setUniverseDomain("universeDomain")
692           .setServiceAccountImpersonationOptions(invalidOptionsMap)
693           .build();
694       fail("Should not be able to continue without exception.");
695     } catch (IllegalArgumentException exception) {
696       assertEquals(
697           "Value of \"token_lifetime_seconds\" field could not be parsed into an integer.",
698           exception.getMessage());
699       assertEquals(NumberFormatException.class, exception.getCause().getClass());
700     }
701   }
702 
703   @Test
constructor_builder_stringTokenLifetime()704   public void constructor_builder_stringTokenLifetime() {
705     Map<String, Object> optionsMap = new HashMap<String, Object>();
706     optionsMap.put("token_lifetime_seconds", "2800");
707 
708     ExternalAccountCredentials credentials =
709         IdentityPoolCredentials.newBuilder()
710             .setHttpTransportFactory(transportFactory)
711             .setAudience(
712                 "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider")
713             .setSubjectTokenType("subjectTokenType")
714             .setTokenUrl(STS_URL)
715             .setTokenInfoUrl("https://tokeninfo.com")
716             .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
717             .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP))
718             .setScopes(Arrays.asList("scope1", "scope2"))
719             .setQuotaProjectId("projectId")
720             .setClientId("clientId")
721             .setClientSecret("clientSecret")
722             .setWorkforcePoolUserProject("workforcePoolUserProject")
723             .setUniverseDomain("universeDomain")
724             .setServiceAccountImpersonationOptions(optionsMap)
725             .build();
726 
727     assertEquals(2800, credentials.getServiceAccountImpersonationOptions().getLifetime());
728   }
729 
730   @Test
constructor_builder_bigDecimalTokenLifetime()731   public void constructor_builder_bigDecimalTokenLifetime() {
732     Map<String, Object> optionsMap = new HashMap<String, Object>();
733     optionsMap.put("token_lifetime_seconds", new BigDecimal("2800"));
734 
735     ExternalAccountCredentials credentials =
736         IdentityPoolCredentials.newBuilder()
737             .setHttpTransportFactory(transportFactory)
738             .setAudience(
739                 "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider")
740             .setSubjectTokenType("subjectTokenType")
741             .setTokenUrl(STS_URL)
742             .setTokenInfoUrl("https://tokeninfo.com")
743             .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
744             .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP))
745             .setScopes(Arrays.asList("scope1", "scope2"))
746             .setQuotaProjectId("projectId")
747             .setClientId("clientId")
748             .setClientSecret("clientSecret")
749             .setWorkforcePoolUserProject("workforcePoolUserProject")
750             .setUniverseDomain("universeDomain")
751             .setServiceAccountImpersonationOptions(optionsMap)
752             .build();
753 
754     assertEquals(2800, credentials.getServiceAccountImpersonationOptions().getLifetime());
755   }
756 
757   @Test
constructor_builder_integerTokenLifetime()758   public void constructor_builder_integerTokenLifetime() {
759     Map<String, Object> optionsMap = new HashMap<String, Object>();
760     optionsMap.put("token_lifetime_seconds", Integer.valueOf(2800));
761 
762     ExternalAccountCredentials credentials =
763         IdentityPoolCredentials.newBuilder()
764             .setHttpTransportFactory(transportFactory)
765             .setAudience(
766                 "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider")
767             .setSubjectTokenType("subjectTokenType")
768             .setTokenUrl(STS_URL)
769             .setTokenInfoUrl("https://tokeninfo.com")
770             .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
771             .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP))
772             .setScopes(Arrays.asList("scope1", "scope2"))
773             .setQuotaProjectId("projectId")
774             .setClientId("clientId")
775             .setClientSecret("clientSecret")
776             .setWorkforcePoolUserProject("workforcePoolUserProject")
777             .setUniverseDomain("universeDomain")
778             .setServiceAccountImpersonationOptions(optionsMap)
779             .build();
780 
781     assertEquals(2800, credentials.getServiceAccountImpersonationOptions().getLifetime());
782   }
783 
784   @Test
constructor_builder_lowTokenLifetime_throws()785   public void constructor_builder_lowTokenLifetime_throws() {
786     Map<String, Object> optionsMap = new HashMap<String, Object>();
787     optionsMap.put("token_lifetime_seconds", 599);
788 
789     try {
790       IdentityPoolCredentials.newBuilder()
791           .setHttpTransportFactory(transportFactory)
792           .setAudience(
793               "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider")
794           .setSubjectTokenType("subjectTokenType")
795           .setTokenUrl(STS_URL)
796           .setTokenInfoUrl("https://tokeninfo.com")
797           .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
798           .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP))
799           .setScopes(Arrays.asList("scope1", "scope2"))
800           .setQuotaProjectId("projectId")
801           .setClientId("clientId")
802           .setClientSecret("clientSecret")
803           .setWorkforcePoolUserProject("workforcePoolUserProject")
804           .setUniverseDomain("universeDomain")
805           .setServiceAccountImpersonationOptions(optionsMap)
806           .build();
807     } catch (IllegalArgumentException e) {
808       assertEquals(
809           "The \"token_lifetime_seconds\" field must be between 600 and 43200 seconds.",
810           e.getMessage());
811     }
812   }
813 
814   @Test
constructor_builder_highTokenLifetime_throws()815   public void constructor_builder_highTokenLifetime_throws() {
816     Map<String, Object> optionsMap = new HashMap<String, Object>();
817     optionsMap.put("token_lifetime_seconds", 43201);
818 
819     try {
820       IdentityPoolCredentials.newBuilder()
821           .setHttpTransportFactory(transportFactory)
822           .setAudience(
823               "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider")
824           .setSubjectTokenType("subjectTokenType")
825           .setTokenUrl(STS_URL)
826           .setTokenInfoUrl("https://tokeninfo.com")
827           .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL)
828           .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP))
829           .setScopes(Arrays.asList("scope1", "scope2"))
830           .setQuotaProjectId("projectId")
831           .setClientId("clientId")
832           .setClientSecret("clientSecret")
833           .setWorkforcePoolUserProject("workforcePoolUserProject")
834           .setUniverseDomain("universeDomain")
835           .setServiceAccountImpersonationOptions(optionsMap)
836           .build();
837     } catch (IllegalArgumentException e) {
838       assertEquals(
839           "The \"token_lifetime_seconds\" field must be between 600 and 43200 seconds.",
840           e.getMessage());
841     }
842   }
843 
844   @Test
exchangeExternalCredentialForAccessToken()845   public void exchangeExternalCredentialForAccessToken() throws IOException {
846     ExternalAccountCredentials credential =
847         ExternalAccountCredentials.fromJson(buildJsonIdentityPoolCredential(), transportFactory);
848 
849     StsTokenExchangeRequest stsTokenExchangeRequest =
850         StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build();
851 
852     AccessToken accessToken =
853         credential.exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest);
854 
855     assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());
856 
857     // Validate no internal options set.
858     Map<String, String> query =
859         TestUtils.parseQuery(transportFactory.transport.getLastRequest().getContentAsString());
860     assertNull(query.get("options"));
861 
862     // Validate metrics header is set correctly on the sts request.
863     Map<String, List<String>> headers =
864         transportFactory.transport.getRequests().get(0).getHeaders();
865     validateMetricsHeader(headers, "file", false, false);
866   }
867 
868   @Test
exchangeExternalCredentialForAccessToken_withInternalOptions()869   public void exchangeExternalCredentialForAccessToken_withInternalOptions() throws IOException {
870     ExternalAccountCredentials credential =
871         ExternalAccountCredentials.fromJson(buildJsonIdentityPoolCredential(), transportFactory);
872 
873     GenericJson internalOptions = new GenericJson();
874     internalOptions.setFactory(OAuth2Utils.JSON_FACTORY);
875     internalOptions.put("key", "value");
876     StsTokenExchangeRequest stsTokenExchangeRequest =
877         StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType")
878             .setInternalOptions(internalOptions.toString())
879             .build();
880 
881     AccessToken accessToken =
882         credential.exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest);
883 
884     assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());
885 
886     // Validate internal options set.
887     Map<String, String> query =
888         TestUtils.parseQuery(transportFactory.transport.getLastRequest().getContentAsString());
889     assertNotNull(query.get("options"));
890     assertEquals(internalOptions.toString(), query.get("options"));
891   }
892 
893   @Test
exchangeExternalCredentialForAccessToken_workforceCred_expectUserProjectPassedToSts()894   public void exchangeExternalCredentialForAccessToken_workforceCred_expectUserProjectPassedToSts()
895       throws IOException {
896     ExternalAccountCredentials identityPoolCredential =
897         ExternalAccountCredentials.fromJson(
898             buildJsonIdentityPoolWorkforceCredential(), transportFactory);
899 
900     ExternalAccountCredentials pluggableAuthCredential =
901         ExternalAccountCredentials.fromJson(
902             buildJsonPluggableAuthWorkforceCredential(), transportFactory);
903 
904     List<ExternalAccountCredentials> credentials =
905         Arrays.asList(identityPoolCredential, pluggableAuthCredential);
906 
907     for (int i = 0; i < credentials.size(); i++) {
908       StsTokenExchangeRequest stsTokenExchangeRequest =
909           StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build();
910 
911       AccessToken accessToken =
912           credentials.get(i).exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest);
913 
914       assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());
915 
916       // Validate internal options set.
917       Map<String, String> query =
918           TestUtils.parseQuery(transportFactory.transport.getLastRequest().getContentAsString());
919       GenericJson internalOptions = new GenericJson();
920       internalOptions.setFactory(OAuth2Utils.JSON_FACTORY);
921       internalOptions.put("userProject", "userProject");
922       assertEquals(internalOptions.toString(), query.get("options"));
923       assertEquals(i + 1, transportFactory.transport.getRequests().size());
924     }
925   }
926 
927   @Test
928   public void
exchangeExternalCredentialForAccessToken_workforceCredWithInternalOptions_expectOverridden()929       exchangeExternalCredentialForAccessToken_workforceCredWithInternalOptions_expectOverridden()
930           throws IOException {
931     ExternalAccountCredentials credential =
932         ExternalAccountCredentials.fromJson(
933             buildJsonIdentityPoolWorkforceCredential(), transportFactory);
934 
935     GenericJson internalOptions = new GenericJson();
936     internalOptions.put("key", "value");
937     StsTokenExchangeRequest stsTokenExchangeRequest =
938         StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType")
939             .setInternalOptions(internalOptions.toString())
940             .build();
941 
942     AccessToken accessToken =
943         credential.exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest);
944 
945     assertEquals(transportFactory.transport.getAccessToken(), accessToken.getTokenValue());
946 
947     // Validate internal options set.
948     Map<String, String> query =
949         TestUtils.parseQuery(transportFactory.transport.getLastRequest().getContentAsString());
950     assertNotNull(query.get("options"));
951     assertEquals(internalOptions.toString(), query.get("options"));
952   }
953 
954   @Test
exchangeExternalCredentialForAccessToken_withServiceAccountImpersonation()955   public void exchangeExternalCredentialForAccessToken_withServiceAccountImpersonation()
956       throws IOException {
957     transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime());
958 
959     ExternalAccountCredentials credential =
960         ExternalAccountCredentials.fromStream(
961             IdentityPoolCredentialsTest.writeIdentityPoolCredentialsStream(
962                 transportFactory.transport.getStsUrl(),
963                 transportFactory.transport.getMetadataUrl(),
964                 transportFactory.transport.getServiceAccountImpersonationUrl(),
965                 /* serviceAccountImpersonationOptionsMap= */ null),
966             transportFactory);
967 
968     StsTokenExchangeRequest stsTokenExchangeRequest =
969         StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build();
970 
971     AccessToken returnedToken =
972         credential.exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest);
973 
974     assertEquals(
975         transportFactory.transport.getServiceAccountAccessToken(), returnedToken.getTokenValue());
976 
977     // Validate that default lifetime was set correctly on the request.
978     GenericJson query =
979         OAuth2Utils.JSON_FACTORY
980             .createJsonParser(transportFactory.transport.getLastRequest().getContentAsString())
981             .parseAndClose(GenericJson.class);
982 
983     assertEquals("3600s", query.get("lifetime"));
984 
985     // Validate metrics header is set correctly on the sts request.
986     Map<String, List<String>> headers =
987         transportFactory.transport.getRequests().get(1).getHeaders();
988     validateMetricsHeader(headers, "url", true, false);
989   }
990 
991   @Test
exchangeExternalCredentialForAccessToken_withServiceAccountImpersonationOptions()992   public void exchangeExternalCredentialForAccessToken_withServiceAccountImpersonationOptions()
993       throws IOException {
994     transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime());
995 
996     ExternalAccountCredentials credential =
997         ExternalAccountCredentials.fromStream(
998             IdentityPoolCredentialsTest.writeIdentityPoolCredentialsStream(
999                 transportFactory.transport.getStsUrl(),
1000                 transportFactory.transport.getMetadataUrl(),
1001                 transportFactory.transport.getServiceAccountImpersonationUrl(),
1002                 buildServiceAccountImpersonationOptions(2800)),
1003             transportFactory);
1004 
1005     StsTokenExchangeRequest stsTokenExchangeRequest =
1006         StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build();
1007 
1008     AccessToken returnedToken =
1009         credential.exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest);
1010 
1011     assertEquals(
1012         transportFactory.transport.getServiceAccountAccessToken(), returnedToken.getTokenValue());
1013 
1014     // Validate that lifetime was set correctly on the request.
1015     GenericJson query =
1016         OAuth2Utils.JSON_FACTORY
1017             .createJsonParser(transportFactory.transport.getLastRequest().getContentAsString())
1018             .parseAndClose(GenericJson.class);
1019 
1020     // Validate metrics header is set correctly on the sts request.
1021     Map<String, List<String>> headers =
1022         transportFactory.transport.getRequests().get(1).getHeaders();
1023     validateMetricsHeader(headers, "url", true, true);
1024     assertEquals("2800s", query.get("lifetime"));
1025   }
1026 
1027   @Test
exchangeExternalCredentialForAccessToken_throws()1028   public void exchangeExternalCredentialForAccessToken_throws() throws IOException {
1029     ExternalAccountCredentials credential =
1030         ExternalAccountCredentials.fromJson(buildJsonIdentityPoolCredential(), transportFactory);
1031 
1032     String errorCode = "invalidRequest";
1033     String errorDescription = "errorDescription";
1034     String errorUri = "errorUri";
1035     transportFactory.transport.addResponseErrorSequence(
1036         TestUtils.buildHttpResponseException(errorCode, errorDescription, errorUri));
1037 
1038     StsTokenExchangeRequest stsTokenExchangeRequest =
1039         StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build();
1040 
1041     try {
1042       credential.exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest);
1043       fail("Exception should be thrown.");
1044     } catch (OAuthException e) {
1045       assertEquals(errorCode, e.getErrorCode());
1046       assertEquals(errorDescription, e.getErrorDescription());
1047       assertEquals(errorUri, e.getErrorUri());
1048     }
1049   }
1050 
1051   @Test
exchangeExternalCredentialForAccessToken_invalidImpersonatedCredentialsThrows()1052   public void exchangeExternalCredentialForAccessToken_invalidImpersonatedCredentialsThrows()
1053       throws IOException {
1054     GenericJson json = buildJsonIdentityPoolCredential();
1055     json.put("service_account_impersonation_url", "https://iamcredentials.googleapis.com");
1056     ExternalAccountCredentials credential =
1057         ExternalAccountCredentials.fromJson(json, transportFactory);
1058 
1059     StsTokenExchangeRequest stsTokenExchangeRequest =
1060         StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build();
1061 
1062     try {
1063       credential.exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest);
1064       fail("Exception should be thrown.");
1065     } catch (IllegalArgumentException e) {
1066       assertEquals(
1067           "Unable to determine target principal from service account impersonation URL.",
1068           e.getMessage());
1069     }
1070   }
1071 
1072   @Test
getRequestMetadata_withQuotaProjectId()1073   public void getRequestMetadata_withQuotaProjectId() throws IOException {
1074     TestExternalAccountCredentials testCredentials =
1075         (TestExternalAccountCredentials)
1076             TestExternalAccountCredentials.newBuilder()
1077                 .setHttpTransportFactory(transportFactory)
1078                 .setAudience("audience")
1079                 .setSubjectTokenType("subjectTokenType")
1080                 .setTokenUrl(STS_URL)
1081                 .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP))
1082                 .setQuotaProjectId("quotaProjectId")
1083                 .build();
1084 
1085     Map<String, List<String>> requestMetadata =
1086         testCredentials.getRequestMetadata(URI.create("http://googleapis.com/foo/bar"));
1087 
1088     assertEquals("quotaProjectId", requestMetadata.get("x-goog-user-project").get(0));
1089   }
1090 
1091   @Test
serialize()1092   public void serialize() throws IOException, ClassNotFoundException {
1093     Map<String, Object> impersonationOpts =
1094         new HashMap<String, Object>() {
1095           {
1096             put("token_lifetime_seconds", 1000);
1097           }
1098         };
1099 
1100     TestExternalAccountCredentials testCredentials =
1101         (TestExternalAccountCredentials)
1102             TestExternalAccountCredentials.newBuilder()
1103                 .setHttpTransportFactory(transportFactory)
1104                 .setAudience("audience")
1105                 .setSubjectTokenType("subjectTokenType")
1106                 .setTokenUrl(STS_URL)
1107                 .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP))
1108                 .setServiceAccountImpersonationOptions(impersonationOpts)
1109                 .build();
1110 
1111     TestExternalAccountCredentials deserializedCredentials =
1112         serializeAndDeserialize(testCredentials);
1113     assertEquals(testCredentials, deserializedCredentials);
1114     assertEquals(testCredentials.hashCode(), deserializedCredentials.hashCode());
1115     assertEquals(testCredentials.toString(), deserializedCredentials.toString());
1116     assertEquals(
1117         testCredentials.getServiceAccountImpersonationOptions().getLifetime(),
1118         deserializedCredentials.getServiceAccountImpersonationOptions().getLifetime());
1119     assertSame(deserializedCredentials.clock, Clock.SYSTEM);
1120   }
1121 
1122   @Test
validateTokenUrl_validUrls()1123   public void validateTokenUrl_validUrls() {
1124     List<String> validUrls =
1125         Arrays.asList(
1126             "https://sts.googleapis.com",
1127             "https://us-east-1.sts.googleapis.com",
1128             "https://US-EAST-1.sts.googleapis.com",
1129             "https://sts.us-east-1.googleapis.com",
1130             "https://sts.US-WEST-1.googleapis.com",
1131             "https://us-east-1-sts.googleapis.com",
1132             "https://US-WEST-1-sts.googleapis.com",
1133             "https://us-west-1-sts.googleapis.com/path?query",
1134             "https://sts-xyz123.p.googleapis.com/path?query",
1135             "https://sts-xyz123.p.googleapis.com",
1136             "https://sts-xyz-123.p.googleapis.com");
1137 
1138     for (String url : validUrls) {
1139       ExternalAccountCredentials.validateTokenUrl(url);
1140       ExternalAccountCredentials.validateTokenUrl(url.toUpperCase(Locale.US));
1141     }
1142   }
1143 
1144   @Test
validateTokenUrl_invalidUrls()1145   public void validateTokenUrl_invalidUrls() {
1146     List<String> invalidUrls =
1147         Arrays.asList(
1148             "sts.googleapis.com",
1149             "https://",
1150             "http://sts.googleapis.com",
1151             "https://us-eas\\t-1.sts.googleapis.com",
1152             "https:/us-east-1.sts.googleapis.com",
1153             "testhttps://us-east-1.sts.googleapis.com",
1154             "hhttps://us-east-1.sts.googleapis.com",
1155             "https://us- -1.sts.googleapis.com");
1156 
1157     for (String url : invalidUrls) {
1158       try {
1159         ExternalAccountCredentials.validateTokenUrl(url);
1160         fail("Should have failed since an invalid URL was passed.");
1161       } catch (IllegalArgumentException e) {
1162         assertEquals("The provided token URL is invalid.", e.getMessage());
1163       }
1164     }
1165   }
1166 
1167   @Test
validateServiceAccountImpersonationUrls_validUrls()1168   public void validateServiceAccountImpersonationUrls_validUrls() {
1169     List<String> validUrls =
1170         Arrays.asList(
1171             "https://iamcredentials.googleapis.com",
1172             "https://us-east-1.iamcredentials.googleapis.com",
1173             "https://US-EAST-1.iamcredentials.googleapis.com",
1174             "https://iamcredentials.us-east-1.googleapis.com",
1175             "https://iamcredentials.US-WEST-1.googleapis.com",
1176             "https://us-east-1-iamcredentials.googleapis.com",
1177             "https://US-WEST-1-iamcredentials.googleapis.com",
1178             "https://us-west-1-iamcredentials.googleapis.com/path?query",
1179             "https://iamcredentials-xyz123.p.googleapis.com/path?query",
1180             "https://iamcredentials-xyz123.p.googleapis.com",
1181             "https://iamcredentials-xyz-123.p.googleapis.com");
1182 
1183     for (String url : validUrls) {
1184       ExternalAccountCredentials.validateServiceAccountImpersonationInfoUrl(url);
1185       ExternalAccountCredentials.validateServiceAccountImpersonationInfoUrl(
1186           url.toUpperCase(Locale.US));
1187     }
1188   }
1189 
1190   @Test
validateServiceAccountImpersonationUrls_invalidUrls()1191   public void validateServiceAccountImpersonationUrls_invalidUrls() {
1192     List<String> invalidUrls =
1193         Arrays.asList(
1194             "iamcredentials.googleapis.com",
1195             "https://",
1196             "http://iamcredentials.googleapis.com",
1197             "https:/iamcredentials.googleapis.com",
1198             "https://us-eas\t-1.iamcredentials.googleapis.com",
1199             "testhttps://us-east-1.iamcredentials.googleapis.com",
1200             "hhttps://us-east-1.iamcredentials.googleapis.com",
1201             "https://us- -1.iamcredentials.googleapis.com");
1202 
1203     for (String url : invalidUrls) {
1204       try {
1205         ExternalAccountCredentials.validateServiceAccountImpersonationInfoUrl(url);
1206         fail("Should have failed since an invalid URL was passed.");
1207       } catch (IllegalArgumentException e) {
1208         assertEquals("The provided service account impersonation URL is invalid.", e.getMessage());
1209       }
1210     }
1211   }
1212 
buildJsonIdentityPoolCredential()1213   private GenericJson buildJsonIdentityPoolCredential() {
1214     GenericJson json = new GenericJson();
1215     json.put(
1216         "audience",
1217         "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider");
1218     json.put("subject_token_type", "subjectTokenType");
1219     json.put("token_url", STS_URL);
1220     json.put("token_info_url", "tokenInfoUrl");
1221 
1222     Map<String, String> map = new HashMap<>();
1223     map.put("file", "file");
1224     json.put("credential_source", map);
1225     return json;
1226   }
1227 
buildJsonIdentityPoolWorkforceCredential()1228   private GenericJson buildJsonIdentityPoolWorkforceCredential() {
1229     GenericJson json = buildJsonIdentityPoolCredential();
1230     json.put(
1231         "audience", "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider");
1232     json.put("workforce_pool_user_project", "userProject");
1233     return json;
1234   }
1235 
buildJsonAwsCredential()1236   private GenericJson buildJsonAwsCredential() {
1237     GenericJson json = new GenericJson();
1238     json.put("audience", "audience");
1239     json.put("subject_token_type", "subjectTokenType");
1240     json.put("token_url", STS_URL);
1241     json.put("token_info_url", "tokenInfoUrl");
1242 
1243     Map<String, String> map = new HashMap<>();
1244     map.put("environment_id", "aws1");
1245     map.put("region_url", "https://169.254.169.254/region");
1246     map.put("url", "https://169.254.169.254/");
1247     map.put("regional_cred_verification_url", "regionalCredVerificationUrl");
1248     json.put("credential_source", map);
1249 
1250     return json;
1251   }
1252 
buildJsonPluggableAuthCredential()1253   private GenericJson buildJsonPluggableAuthCredential() {
1254     GenericJson json = new GenericJson();
1255     json.put("audience", "audience");
1256     json.put("subject_token_type", "subjectTokenType");
1257     json.put("token_url", STS_URL);
1258     json.put("token_info_url", "tokenInfoUrl");
1259 
1260     Map<String, Map<String, Object>> credentialSource = new HashMap<>();
1261 
1262     Map<String, Object> executableConfig = new HashMap<>();
1263     executableConfig.put("command", "command");
1264 
1265     credentialSource.put("executable", executableConfig);
1266     json.put("credential_source", credentialSource);
1267 
1268     return json;
1269   }
1270 
buildJsonPluggableAuthWorkforceCredential()1271   private GenericJson buildJsonPluggableAuthWorkforceCredential() {
1272     GenericJson json = buildJsonPluggableAuthCredential();
1273     json.put(
1274         "audience", "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider");
1275     json.put("workforce_pool_user_project", "userProject");
1276     return json;
1277   }
1278 
buildServiceAccountImpersonationOptions(Integer lifetime)1279   static Map<String, Object> buildServiceAccountImpersonationOptions(Integer lifetime) {
1280     Map<String, Object> map = new HashMap<String, Object>();
1281     map.put("token_lifetime_seconds", lifetime);
1282 
1283     return map;
1284   }
1285 
validateMetricsHeader( Map<String, List<String>> headers, String source, boolean saImpersonationUsed, boolean configLifetimeUsed)1286   static void validateMetricsHeader(
1287       Map<String, List<String>> headers,
1288       String source,
1289       boolean saImpersonationUsed,
1290       boolean configLifetimeUsed) {
1291     assertTrue(headers.containsKey(MetricsUtils.API_CLIENT_HEADER));
1292     String actualMetricsValue = headers.get(MetricsUtils.API_CLIENT_HEADER).get(0);
1293     String expectedMetricsValue =
1294         String.format(
1295             "%s google-byoid-sdk source/%s sa-impersonation/%s config-lifetime/%s",
1296             MetricsUtils.getLanguageAndAuthLibraryVersions(),
1297             source,
1298             saImpersonationUsed,
1299             configLifetimeUsed);
1300     assertEquals(expectedMetricsValue, actualMetricsValue);
1301   }
1302 
1303   static class TestExternalAccountCredentials extends ExternalAccountCredentials {
1304     static class TestCredentialSource extends IdentityPoolCredentialSource {
TestCredentialSource(Map<String, Object> credentialSourceMap)1305       protected TestCredentialSource(Map<String, Object> credentialSourceMap) {
1306         super(credentialSourceMap);
1307       }
1308     }
1309 
newBuilder()1310     public static Builder newBuilder() {
1311       return new Builder();
1312     }
1313 
1314     static class Builder extends ExternalAccountCredentials.Builder {
Builder()1315       Builder() {}
1316 
1317       @Override
build()1318       public TestExternalAccountCredentials build() {
1319         return new TestExternalAccountCredentials(this);
1320       }
1321     }
1322 
TestExternalAccountCredentials(ExternalAccountCredentials.Builder builder)1323     protected TestExternalAccountCredentials(ExternalAccountCredentials.Builder builder) {
1324       super(builder);
1325     }
1326 
1327     @Override
refreshAccessToken()1328     public AccessToken refreshAccessToken() {
1329       return new AccessToken("accessToken", new Date());
1330     }
1331 
1332     @Override
retrieveSubjectToken()1333     public String retrieveSubjectToken() {
1334       return "subjectToken";
1335     }
1336   }
1337 }
1338