From c5367d5bfbe84210e41c27a9227e97ceaa00bf72 Mon Sep 17 00:00:00 2001 From: aeitzman Date: Thu, 18 Aug 2022 15:28:17 -0700 Subject: [PATCH 1/8] feat: adding configurable token lifespan support and tests --- .../oauth2/ExternalAccountCredentials.java | 115 ++++++++++++++++- .../ExternalAccountCredentialsTest.java | 118 +++++++++++++++++- .../auth/oauth2/GoogleCredentialsTest.java | 3 +- .../ITWorkloadIdentityFederationTest.java | 25 ++++ .../oauth2/IdentityPoolCredentialsTest.java | 9 +- 5 files changed, 263 insertions(+), 7 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index 0d6f3419b..ce8352c8f 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -43,11 +43,13 @@ import com.google.common.base.MoreObjects; import java.io.IOException; import java.io.InputStream; +import java.math.BigDecimal; import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -73,8 +75,47 @@ abstract static class CredentialSource { } } + /** + * Encapsulates the service account impersonation options portion of the configuration for + * ExternalAccountCredentials. + * + *

If token_lifetime_seconds is not specified, the library will + * default to a 1-hour lifetime. + * + *

+   * Sample credential source for Pluggable Auth credentials:
+   * {
+   *   ...
+   *   "service_account_impersonation": {
+   *     "token_lifetime_seconds": 2800
+   *    }
+   * }
+   * 
+ */ + static class ServiceAccountImpersonationOptions { + public final int lifetime; + + ServiceAccountImpersonationOptions(Map serviceAccountImpersonationOptionsMap) { + if (serviceAccountImpersonationOptionsMap.containsKey(TOKEN_LIFETIME_SECONDS_KEY)) { + Object timeout = serviceAccountImpersonationOptionsMap.get(TOKEN_LIFETIME_SECONDS_KEY); + if (timeout instanceof BigDecimal) { + lifetime = ((BigDecimal) timeout).intValue(); + } else if (serviceAccountImpersonationOptionsMap.get(TOKEN_LIFETIME_SECONDS_KEY) instanceof Integer) { + lifetime = (int) timeout; + } else { + lifetime = Integer.parseInt((String) timeout); + } + } + else{ + lifetime = DEFAULT_TOKEN_LIFETIME_SECONDS; + } + } + } + private static final String CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform"; + private static final int DEFAULT_TOKEN_LIFETIME_SECONDS = 3600; + private static final String TOKEN_LIFETIME_SECONDS_KEY = "token_lifetime_seconds"; static final String EXTERNAL_ACCOUNT_FILE_TYPE = "external_account"; static final String EXECUTABLE_SOURCE_KEY = "executable"; @@ -85,6 +126,7 @@ abstract static class CredentialSource { private final String tokenUrl; private final CredentialSource credentialSource; private final Collection scopes; + private final ServiceAccountImpersonationOptions serviceAccountImpersonationOptions; @Nullable private final String tokenInfoUrl; @Nullable private final String serviceAccountImpersonationUrl; @@ -139,7 +181,7 @@ protected ExternalAccountCredentials( @Nullable String quotaProjectId, @Nullable String clientId, @Nullable String clientSecret, - @Nullable Collection scopes) { + @Nullable Collection scopes){ this( transportFactory, audience, @@ -152,7 +194,8 @@ protected ExternalAccountCredentials( clientId, clientSecret, scopes, - /* environmentProvider= */ null); + /* environmentProvider= */ null, + /* serviceAccountImpersonationOptions= */ null); } /** @@ -175,6 +218,43 @@ protected ExternalAccountCredentials( @Nullable String clientSecret, @Nullable Collection scopes, @Nullable EnvironmentProvider environmentProvider) { + this( + transportFactory, + audience, + subjectTokenType, + tokenUrl, + credentialSource, + tokenInfoUrl, + serviceAccountImpersonationUrl, + quotaProjectId, + clientId, + clientSecret, + scopes, + environmentProvider, + /* serviceAccountImpersonationOptions= */ null); + } + + /** + * See {@link ExternalAccountCredentials#ExternalAccountCredentials(HttpTransportFactory, String, + * String, String, CredentialSource, String, String, String, String, String, Collection, EnvironmentProvider)} + * + * @param serviceAccountImpersonationOptions additional options for service account impersonation, + * may be null. + */ + protected ExternalAccountCredentials( + HttpTransportFactory transportFactory, + String audience, + String subjectTokenType, + String tokenUrl, + CredentialSource credentialSource, + @Nullable String tokenInfoUrl, + @Nullable String serviceAccountImpersonationUrl, + @Nullable String quotaProjectId, + @Nullable String clientId, + @Nullable String clientSecret, + @Nullable Collection scopes, + @Nullable EnvironmentProvider environmentProvider, + @Nullable ServiceAccountImpersonationOptions serviceAccountImpersonationOptions) { this.transportFactory = MoreObjects.firstNonNull( transportFactory, @@ -186,6 +266,10 @@ protected ExternalAccountCredentials( this.credentialSource = checkNotNull(credentialSource); this.tokenInfoUrl = tokenInfoUrl; this.serviceAccountImpersonationUrl = serviceAccountImpersonationUrl; + this.serviceAccountImpersonationOptions = + serviceAccountImpersonationOptions == null + ? new ServiceAccountImpersonationOptions(new HashMap()) + : serviceAccountImpersonationOptions; this.quotaProjectId = quotaProjectId; this.clientId = clientId; this.clientSecret = clientSecret; @@ -230,6 +314,10 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder) builder.environmentProvider == null ? SystemEnvironmentProvider.getInstance() : builder.environmentProvider; + this.serviceAccountImpersonationOptions = + builder.serviceAccountImpersonationOptions == null + ? new ServiceAccountImpersonationOptions(new HashMap()) + : builder.serviceAccountImpersonationOptions; this.workforcePoolUserProject = builder.workforcePoolUserProject; if (workforcePoolUserProject != null && !isWorkforcePoolConfiguration()) { @@ -275,7 +363,7 @@ ImpersonatedCredentials buildImpersonatedCredentials() { .setHttpTransportFactory(transportFactory) .setTargetPrincipal(targetPrincipal) .setScopes(new ArrayList<>(scopes)) - .setLifetime(3600) // 1 hour in seconds + .setLifetime(this.serviceAccountImpersonationOptions.lifetime) .setIamEndpointOverride(serviceAccountImpersonationUrl) .build(); } @@ -375,6 +463,12 @@ static ExternalAccountCredentials fromJson( String clientSecret = (String) json.get("client_secret"); String quotaProjectId = (String) json.get("quota_project_id"); String userProject = (String) json.get("workforce_pool_user_project"); + Map impersonationOptionsMap = (Map) json.get("service_account_impersonation"); + + ServiceAccountImpersonationOptions serviceAccountImpersonationOptions = null; + if (impersonationOptionsMap != null){ + serviceAccountImpersonationOptions = new ServiceAccountImpersonationOptions(impersonationOptionsMap); + } if (isAwsCredential(credentialSourceMap)) { return AwsCredentials.newBuilder() @@ -388,6 +482,7 @@ static ExternalAccountCredentials fromJson( .setQuotaProjectId(quotaProjectId) .setClientId(clientId) .setClientSecret(clientSecret) + .setServiceAccountImpersonationOptions(serviceAccountImpersonationOptions) .build(); } else if (isPluggableAuthCredential(credentialSourceMap)) { return PluggableAuthCredentials.newBuilder() @@ -402,6 +497,7 @@ static ExternalAccountCredentials fromJson( .setClientId(clientId) .setClientSecret(clientSecret) .setWorkforcePoolUserProject(userProject) + .setServiceAccountImpersonationOptions(serviceAccountImpersonationOptions) .build(); } return IdentityPoolCredentials.newBuilder() @@ -416,6 +512,7 @@ static ExternalAccountCredentials fromJson( .setClientId(clientId) .setClientSecret(clientSecret) .setWorkforcePoolUserProject(userProject) + .setServiceAccountImpersonationOptions(serviceAccountImpersonationOptions) .build(); } @@ -539,6 +636,11 @@ public String getWorkforcePoolUserProject() { return workforcePoolUserProject; } + @Nullable + public ServiceAccountImpersonationOptions getServiceAccountImpersonationOptions() { + return serviceAccountImpersonationOptions; + } + EnvironmentProvider getEnvironmentProvider() { return environmentProvider; } @@ -625,6 +727,7 @@ public abstract static class Builder extends GoogleCredentials.Builder { @Nullable protected String clientSecret; @Nullable protected Collection scopes; @Nullable protected String workforcePoolUserProject; + @Nullable protected ServiceAccountImpersonationOptions serviceAccountImpersonationOptions; protected Builder() {} @@ -642,6 +745,7 @@ protected Builder(ExternalAccountCredentials credentials) { this.scopes = credentials.scopes; this.environmentProvider = credentials.environmentProvider; this.workforcePoolUserProject = credentials.workforcePoolUserProject; + this.serviceAccountImpersonationOptions = credentials.serviceAccountImpersonationOptions; } /** Sets the HTTP transport factory, creates the transport used to get access tokens. */ @@ -738,6 +842,11 @@ Builder setEnvironmentProvider(EnvironmentProvider environmentProvider) { return this; } + Builder setServiceAccountImpersonationOptions(ServiceAccountImpersonationOptions serviceAccountImpersonationOptions) { + this.serviceAccountImpersonationOptions = serviceAccountImpersonationOptions; + return this; + } + public abstract ExternalAccountCredentials build(); } } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index 1125ebc51..33102c31e 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -202,6 +202,27 @@ public void fromJson_identityPoolCredentialsWorkforce() { assertNotNull(credential.getCredentialSource()); } + @Test + public void fromJson_identityPoolCredentialsWithServiceAccountImpersonationOptions() { + GenericJson identityPoolCredentialJson = buildJsonIdentityPoolCredential(); + identityPoolCredentialJson.set("service_account_impersonation", buildServiceAccountImpersonationOptions(2800)); + + ExternalAccountCredentials credential = + ExternalAccountCredentials.fromJson(identityPoolCredentialJson, OAuth2Utils.HTTP_TRANSPORT_FACTORY); + + assertTrue(credential instanceof IdentityPoolCredentials); + assertEquals( + "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider", + credential.getAudience()); + assertEquals("subjectTokenType", credential.getSubjectTokenType()); + assertEquals(STS_URL, credential.getTokenUrl()); + assertEquals("tokenInfoUrl", credential.getTokenInfoUrl()); + assertNotNull(credential.getCredentialSource()); + assertEquals( + 2800, + credential.getServiceAccountImpersonationOptions().lifetime); + } + @Test public void fromJson_awsCredentials() throws IOException { ExternalAccountCredentials credential = @@ -216,6 +237,25 @@ public void fromJson_awsCredentials() throws IOException { assertNotNull(credential.getCredentialSource()); } + @Test + public void fromJson_awsCredentialsWithServiceAccountImpersonationOptions() throws IOException { + GenericJson awsCredentialJson = buildJsonAwsCredential(); + awsCredentialJson.set("service_account_impersonation", buildServiceAccountImpersonationOptions(2800)); + + ExternalAccountCredentials credential = + ExternalAccountCredentials.fromJson(awsCredentialJson, OAuth2Utils.HTTP_TRANSPORT_FACTORY); + + assertTrue(credential instanceof AwsCredentials); + assertEquals("audience", credential.getAudience()); + assertEquals("subjectTokenType", credential.getSubjectTokenType()); + assertEquals(STS_URL, credential.getTokenUrl()); + assertEquals("tokenInfoUrl", credential.getTokenInfoUrl()); + assertNotNull(credential.getCredentialSource()); + assertEquals( + 2800, + credential.getServiceAccountImpersonationOptions().lifetime); + } + @Test public void fromJson_pluggableAuthCredentials() { ExternalAccountCredentials credential = @@ -287,6 +327,31 @@ public void fromJson_pluggableAuthCredentials_allExecutableOptionsSet() { assertEquals(5000, source.getTimeoutMs()); } + @Test + public void fromJson_pluggableAuthCredentialsWithServiceAccountImpersonation() { + GenericJson pluggableAuthCredentialJson = buildJsonPluggableAuthCredential(); + pluggableAuthCredentialJson.set("service_account_impersonation", buildServiceAccountImpersonationOptions(2800)); + + ExternalAccountCredentials credential = + ExternalAccountCredentials.fromJson(pluggableAuthCredentialJson, OAuth2Utils.HTTP_TRANSPORT_FACTORY); + + assertTrue(credential instanceof PluggableAuthCredentials); + assertEquals("audience", credential.getAudience()); + assertEquals("subjectTokenType", credential.getSubjectTokenType()); + assertEquals(STS_URL, credential.getTokenUrl()); + assertEquals("tokenInfoUrl", credential.getTokenInfoUrl()); + assertNotNull(credential.getCredentialSource()); + assertEquals( + 2800, + credential.getServiceAccountImpersonationOptions().lifetime); + + PluggableAuthCredentialSource source = + (PluggableAuthCredentialSource) credential.getCredentialSource(); + assertEquals("command", source.getCommand()); + assertEquals(30000, source.getTimeoutMs()); // Default timeout is 30s. + assertNull(source.getOutputFilePath()); + } + @Test public void fromJson_nullJson_throws() { try { @@ -590,7 +655,8 @@ public void exchangeExternalCredentialForAccessToken_withServiceAccountImpersona IdentityPoolCredentialsTest.writeIdentityPoolCredentialsStream( transportFactory.transport.getStsUrl(), transportFactory.transport.getMetadataUrl(), - transportFactory.transport.getServiceAccountImpersonationUrl()), + transportFactory.transport.getServiceAccountImpersonationUrl(), + /* serviceAccountImpersonationOptionsMap= */ null), transportFactory); StsTokenExchangeRequest stsTokenExchangeRequest = @@ -601,6 +667,46 @@ public void exchangeExternalCredentialForAccessToken_withServiceAccountImpersona assertEquals( transportFactory.transport.getServiceAccountAccessToken(), returnedToken.getTokenValue()); + + // Validate that default lifetime was set correctly on the request. + GenericJson query = + OAuth2Utils.JSON_FACTORY + .createJsonParser(transportFactory.transport.getLastRequest().getContentAsString()) + .parseAndClose(GenericJson.class); + + assertEquals("3600s", query.get("lifetime")); + } + + @Test + public void exchangeExternalCredentialForAccessToken_withServiceAccountImpersonationOptions() + throws IOException { + transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); + + ExternalAccountCredentials credential = + ExternalAccountCredentials.fromStream( + IdentityPoolCredentialsTest.writeIdentityPoolCredentialsStream( + transportFactory.transport.getStsUrl(), + transportFactory.transport.getMetadataUrl(), + transportFactory.transport.getServiceAccountImpersonationUrl(), + buildServiceAccountImpersonationOptions(2800)), + transportFactory); + + StsTokenExchangeRequest stsTokenExchangeRequest = + StsTokenExchangeRequest.newBuilder("credential", "subjectTokenType").build(); + + AccessToken returnedToken = + credential.exchangeExternalCredentialForAccessToken(stsTokenExchangeRequest); + + assertEquals( + transportFactory.transport.getServiceAccountAccessToken(), returnedToken.getTokenValue()); + + // Validate that lifetime was set correctly on the request. + GenericJson query = + OAuth2Utils.JSON_FACTORY + .createJsonParser(transportFactory.transport.getLastRequest().getContentAsString()) + .parseAndClose(GenericJson.class); + + assertEquals("2800s", query.get("lifetime")); } @Test @@ -614,7 +720,8 @@ public void exchangeExternalCredentialForAccessToken_withServiceAccountImpersona IdentityPoolCredentialsTest.writeIdentityPoolCredentialsStream( transportFactory.transport.getStsUrl(), transportFactory.transport.getMetadataUrl(), - transportFactory.transport.getServiceAccountImpersonationUrl()), + transportFactory.transport.getServiceAccountImpersonationUrl(), + /* serviceAccountImpersonationOptionsMap= */ null), transportFactory); // Override impersonated credentials. @@ -851,6 +958,13 @@ private GenericJson buildJsonPluggableAuthWorkforceCredential() { return json; } + private Map buildServiceAccountImpersonationOptions(Integer lifetime) { + Map map = new HashMap(); + map.put("token_lifetime_seconds", lifetime); + + return map; + } + static class TestExternalAccountCredentials extends ExternalAccountCredentials { static class TestCredentialSource extends IdentityPoolCredentials.IdentityPoolCredentialSource { protected TestCredentialSource(Map credentialSourceMap) { diff --git a/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java index 551aecf43..f849ccbb1 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/GoogleCredentialsTest.java @@ -241,7 +241,8 @@ public void fromStream_identityPoolCredentials_providesToken() throws IOExceptio IdentityPoolCredentialsTest.writeIdentityPoolCredentialsStream( transportFactory.transport.getStsUrl(), transportFactory.transport.getMetadataUrl(), - /* serviceAccountImpersonationUrl= */ null); + /* serviceAccountImpersonationUrl= */ null, + /* serviceAccountImpersonationOptionsMap= */ null); GoogleCredentials credentials = GoogleCredentials.fromStream(identityPoolCredentialStream, transportFactory); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java index 3ab290993..0079e6192 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java @@ -52,6 +52,8 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.time.Instant; +import java.util.HashMap; +import java.util.Map; import org.junit.Before; import org.junit.Test; @@ -168,6 +170,29 @@ public void pluggableAuthCredentials() throws IOException { callGcs(pluggableAuthCredentials); } + /** + * Sets the service account impersonation object in configuration JSON with a non-default value + * for token_lifetime_seconds and validates that the lifetime is used for the access token. + */ + @Test + public void IdentityPoolCredentialsWithServiceAccountImpersonationOptions() throws IOException { + GenericJson identityPoolCredentialConfig = buildIdentityPoolCredentialConfig(); + Map map = new HashMap(); + map.put("token_lifetime_seconds", 2800); + identityPoolCredentialConfig.put("service_account_impersonation", map); + + IdentityPoolCredentials identityPoolCredentials = + (IdentityPoolCredentials) + ExternalAccountCredentials.fromJson( + identityPoolCredentialConfig, OAuth2Utils.HTTP_TRANSPORT_FACTORY); + long maxExpirationTime = Instant.now().plusSeconds(2800 + 5).toEpochMilli(); + long minExpirationtime = Instant.now().plusSeconds(2800 - 5).toEpochMilli(); + + callGcs(identityPoolCredentials); + long tokenExpiry = identityPoolCredentials.getAccessToken().getExpirationTimeMillis(); + assertTrue(minExpirationtime <= tokenExpiry && tokenExpiry <= maxExpirationTime); + } + private GenericJson buildIdentityPoolCredentialConfig() throws IOException { String idToken = generateGoogleIdToken(OIDC_AUDIENCE); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java index e7d48978e..5efbb3b36 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java @@ -663,7 +663,10 @@ public void builder_emptyWorkforceUserProjectWithWorkforceAudience() { } static InputStream writeIdentityPoolCredentialsStream( - String tokenUrl, String url, @Nullable String serviceAccountImpersonationUrl) + String tokenUrl, + String url, + @Nullable String serviceAccountImpersonationUrl, + @Nullable Map serviceAccountImpersonationOptionsMap) throws IOException { GenericJson json = new GenericJson(); json.put("audience", "audience"); @@ -676,6 +679,10 @@ static InputStream writeIdentityPoolCredentialsStream( json.put("service_account_impersonation_url", serviceAccountImpersonationUrl); } + if (serviceAccountImpersonationOptionsMap != null) { + json.put("service_account_impersonation", serviceAccountImpersonationOptionsMap); + } + GenericJson credentialSource = new GenericJson(); GenericJson headers = new GenericJson(); headers.put("Metadata-Flavor", "Google"); From 43309e32f4469e7c17b0f6246f848078fb864e7f Mon Sep 17 00:00:00 2001 From: aeitzman Date: Thu, 18 Aug 2022 15:36:50 -0700 Subject: [PATCH 2/8] fix: correcting linting errors --- .../oauth2/ExternalAccountCredentials.java | 31 ++++++++++--------- .../ExternalAccountCredentialsTest.java | 27 ++++++++-------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index ce8352c8f..d22d11143 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -79,8 +79,7 @@ abstract static class CredentialSource { * Encapsulates the service account impersonation options portion of the configuration for * ExternalAccountCredentials. * - *

If token_lifetime_seconds is not specified, the library will - * default to a 1-hour lifetime. + *

If token_lifetime_seconds is not specified, the library will default to a 1-hour lifetime. * *

    * Sample credential source for Pluggable Auth credentials:
@@ -100,13 +99,13 @@ static class ServiceAccountImpersonationOptions {
         Object timeout = serviceAccountImpersonationOptionsMap.get(TOKEN_LIFETIME_SECONDS_KEY);
         if (timeout instanceof BigDecimal) {
           lifetime = ((BigDecimal) timeout).intValue();
-        } else if (serviceAccountImpersonationOptionsMap.get(TOKEN_LIFETIME_SECONDS_KEY) instanceof Integer) {
+        } else if (serviceAccountImpersonationOptionsMap.get(TOKEN_LIFETIME_SECONDS_KEY)
+            instanceof Integer) {
           lifetime = (int) timeout;
         } else {
           lifetime = Integer.parseInt((String) timeout);
         }
-      }
-      else{
+      } else {
         lifetime = DEFAULT_TOKEN_LIFETIME_SECONDS;
       }
     }
@@ -181,7 +180,7 @@ protected ExternalAccountCredentials(
       @Nullable String quotaProjectId,
       @Nullable String clientId,
       @Nullable String clientSecret,
-      @Nullable Collection scopes){
+      @Nullable Collection scopes) {
     this(
         transportFactory,
         audience,
@@ -236,10 +235,11 @@ protected ExternalAccountCredentials(
 
   /**
    * See {@link ExternalAccountCredentials#ExternalAccountCredentials(HttpTransportFactory, String,
-   * String, String, CredentialSource, String, String, String, String, String, Collection, EnvironmentProvider)}
+   * String, String, CredentialSource, String, String, String, String, String, Collection,
+   * EnvironmentProvider)}
    *
    * @param serviceAccountImpersonationOptions additional options for service account impersonation,
-   *    may be null.
+   *     may be null.
    */
   protected ExternalAccountCredentials(
       HttpTransportFactory transportFactory,
@@ -316,8 +316,8 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder)
             : builder.environmentProvider;
     this.serviceAccountImpersonationOptions =
         builder.serviceAccountImpersonationOptions == null
-        ? new ServiceAccountImpersonationOptions(new HashMap())
-        : builder.serviceAccountImpersonationOptions;
+            ? new ServiceAccountImpersonationOptions(new HashMap())
+            : builder.serviceAccountImpersonationOptions;
 
     this.workforcePoolUserProject = builder.workforcePoolUserProject;
     if (workforcePoolUserProject != null && !isWorkforcePoolConfiguration()) {
@@ -463,11 +463,13 @@ static ExternalAccountCredentials fromJson(
     String clientSecret = (String) json.get("client_secret");
     String quotaProjectId = (String) json.get("quota_project_id");
     String userProject = (String) json.get("workforce_pool_user_project");
-    Map impersonationOptionsMap = (Map) json.get("service_account_impersonation");
+    Map impersonationOptionsMap =
+        (Map) json.get("service_account_impersonation");
 
     ServiceAccountImpersonationOptions serviceAccountImpersonationOptions = null;
-    if (impersonationOptionsMap != null){
-      serviceAccountImpersonationOptions = new ServiceAccountImpersonationOptions(impersonationOptionsMap);
+    if (impersonationOptionsMap != null) {
+      serviceAccountImpersonationOptions =
+          new ServiceAccountImpersonationOptions(impersonationOptionsMap);
     }
 
     if (isAwsCredential(credentialSourceMap)) {
@@ -842,7 +844,8 @@ Builder setEnvironmentProvider(EnvironmentProvider environmentProvider) {
       return this;
     }
 
-    Builder setServiceAccountImpersonationOptions(ServiceAccountImpersonationOptions serviceAccountImpersonationOptions) {
+    Builder setServiceAccountImpersonationOptions(
+        ServiceAccountImpersonationOptions serviceAccountImpersonationOptions) {
       this.serviceAccountImpersonationOptions = serviceAccountImpersonationOptions;
       return this;
     }
diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java
index 33102c31e..7b868f7c4 100644
--- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java
+++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java
@@ -205,10 +205,12 @@ public void fromJson_identityPoolCredentialsWorkforce() {
   @Test
   public void fromJson_identityPoolCredentialsWithServiceAccountImpersonationOptions() {
     GenericJson identityPoolCredentialJson = buildJsonIdentityPoolCredential();
-    identityPoolCredentialJson.set("service_account_impersonation", buildServiceAccountImpersonationOptions(2800));
+    identityPoolCredentialJson.set(
+        "service_account_impersonation", buildServiceAccountImpersonationOptions(2800));
 
     ExternalAccountCredentials credential =
-        ExternalAccountCredentials.fromJson(identityPoolCredentialJson, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
+        ExternalAccountCredentials.fromJson(
+            identityPoolCredentialJson, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
 
     assertTrue(credential instanceof IdentityPoolCredentials);
     assertEquals(
@@ -218,9 +220,7 @@ public void fromJson_identityPoolCredentialsWithServiceAccountImpersonationOptio
     assertEquals(STS_URL, credential.getTokenUrl());
     assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
     assertNotNull(credential.getCredentialSource());
-    assertEquals(
-        2800,
-        credential.getServiceAccountImpersonationOptions().lifetime);
+    assertEquals(2800, credential.getServiceAccountImpersonationOptions().lifetime);
   }
 
   @Test
@@ -240,7 +240,8 @@ public void fromJson_awsCredentials() throws IOException {
   @Test
   public void fromJson_awsCredentialsWithServiceAccountImpersonationOptions() throws IOException {
     GenericJson awsCredentialJson = buildJsonAwsCredential();
-    awsCredentialJson.set("service_account_impersonation", buildServiceAccountImpersonationOptions(2800));
+    awsCredentialJson.set(
+        "service_account_impersonation", buildServiceAccountImpersonationOptions(2800));
 
     ExternalAccountCredentials credential =
         ExternalAccountCredentials.fromJson(awsCredentialJson, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
@@ -251,9 +252,7 @@ public void fromJson_awsCredentialsWithServiceAccountImpersonationOptions() thro
     assertEquals(STS_URL, credential.getTokenUrl());
     assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
     assertNotNull(credential.getCredentialSource());
-    assertEquals(
-        2800,
-        credential.getServiceAccountImpersonationOptions().lifetime);
+    assertEquals(2800, credential.getServiceAccountImpersonationOptions().lifetime);
   }
 
   @Test
@@ -330,10 +329,12 @@ public void fromJson_pluggableAuthCredentials_allExecutableOptionsSet() {
   @Test
   public void fromJson_pluggableAuthCredentialsWithServiceAccountImpersonation() {
     GenericJson pluggableAuthCredentialJson = buildJsonPluggableAuthCredential();
-    pluggableAuthCredentialJson.set("service_account_impersonation", buildServiceAccountImpersonationOptions(2800));
+    pluggableAuthCredentialJson.set(
+        "service_account_impersonation", buildServiceAccountImpersonationOptions(2800));
 
     ExternalAccountCredentials credential =
-        ExternalAccountCredentials.fromJson(pluggableAuthCredentialJson, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
+        ExternalAccountCredentials.fromJson(
+            pluggableAuthCredentialJson, OAuth2Utils.HTTP_TRANSPORT_FACTORY);
 
     assertTrue(credential instanceof PluggableAuthCredentials);
     assertEquals("audience", credential.getAudience());
@@ -341,9 +342,7 @@ public void fromJson_pluggableAuthCredentialsWithServiceAccountImpersonation() {
     assertEquals(STS_URL, credential.getTokenUrl());
     assertEquals("tokenInfoUrl", credential.getTokenInfoUrl());
     assertNotNull(credential.getCredentialSource());
-    assertEquals(
-        2800,
-        credential.getServiceAccountImpersonationOptions().lifetime);
+    assertEquals(2800, credential.getServiceAccountImpersonationOptions().lifetime);
 
     PluggableAuthCredentialSource source =
         (PluggableAuthCredentialSource) credential.getCredentialSource();

From 893b39b1e360ce8510b1b5e97932c039ad30032e Mon Sep 17 00:00:00 2001
From: aeitzman 
Date: Mon, 22 Aug 2022 15:16:03 -0700
Subject: [PATCH 3/8] fix: address code review

---
 .../oauth2/ExternalAccountCredentials.java    | 79 +++++--------------
 .../auth/oauth2/AwsCredentialsTest.java       | 36 +++++++++
 .../ExternalAccountCredentialsTest.java       |  2 +-
 .../ITWorkloadIdentityFederationTest.java     |  2 +-
 .../oauth2/IdentityPoolCredentialsTest.java   | 75 ++++++++++++++++++
 .../oauth2/PluggableAuthCredentialsTest.java  | 36 +++++++++
 6 files changed, 169 insertions(+), 61 deletions(-)

diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
index d22d11143..f178608d5 100644
--- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
+++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java
@@ -82,7 +82,7 @@ abstract static class CredentialSource {
    * 

If token_lifetime_seconds is not specified, the library will default to a 1-hour lifetime. * *

-   * Sample credential source for Pluggable Auth credentials:
+   * Sample configuration:
    * {
    *   ...
    *   "service_account_impersonation": {
@@ -92,29 +92,30 @@ abstract static class CredentialSource {
    * 
*/ static class ServiceAccountImpersonationOptions { + private static final int DEFAULT_TOKEN_LIFETIME_SECONDS = 3600; + private static final String TOKEN_LIFETIME_SECONDS_KEY = "token_lifetime_seconds"; + public final int lifetime; - ServiceAccountImpersonationOptions(Map serviceAccountImpersonationOptionsMap) { - if (serviceAccountImpersonationOptionsMap.containsKey(TOKEN_LIFETIME_SECONDS_KEY)) { - Object timeout = serviceAccountImpersonationOptionsMap.get(TOKEN_LIFETIME_SECONDS_KEY); - if (timeout instanceof BigDecimal) { - lifetime = ((BigDecimal) timeout).intValue(); - } else if (serviceAccountImpersonationOptionsMap.get(TOKEN_LIFETIME_SECONDS_KEY) - instanceof Integer) { - lifetime = (int) timeout; - } else { - lifetime = Integer.parseInt((String) timeout); - } - } else { + ServiceAccountImpersonationOptions(Map optionsMap) { + if (!optionsMap.containsKey(TOKEN_LIFETIME_SECONDS_KEY)) { lifetime = DEFAULT_TOKEN_LIFETIME_SECONDS; + return; + } + + Object timeout = optionsMap.get(TOKEN_LIFETIME_SECONDS_KEY); + if (timeout instanceof BigDecimal) { + lifetime = ((BigDecimal) timeout).intValue(); + } else if (optionsMap.get(TOKEN_LIFETIME_SECONDS_KEY) instanceof Integer) { + lifetime = (int) timeout; + } else { + lifetime = Integer.parseInt((String) timeout); } } } private static final String CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform"; - private static final int DEFAULT_TOKEN_LIFETIME_SECONDS = 3600; - private static final String TOKEN_LIFETIME_SECONDS_KEY = "token_lifetime_seconds"; static final String EXTERNAL_ACCOUNT_FILE_TYPE = "external_account"; static final String EXECUTABLE_SOURCE_KEY = "executable"; @@ -193,8 +194,7 @@ protected ExternalAccountCredentials( clientId, clientSecret, scopes, - /* environmentProvider= */ null, - /* serviceAccountImpersonationOptions= */ null); + /* environmentProvider= */ null); } /** @@ -217,44 +217,6 @@ protected ExternalAccountCredentials( @Nullable String clientSecret, @Nullable Collection scopes, @Nullable EnvironmentProvider environmentProvider) { - this( - transportFactory, - audience, - subjectTokenType, - tokenUrl, - credentialSource, - tokenInfoUrl, - serviceAccountImpersonationUrl, - quotaProjectId, - clientId, - clientSecret, - scopes, - environmentProvider, - /* serviceAccountImpersonationOptions= */ null); - } - - /** - * See {@link ExternalAccountCredentials#ExternalAccountCredentials(HttpTransportFactory, String, - * String, String, CredentialSource, String, String, String, String, String, Collection, - * EnvironmentProvider)} - * - * @param serviceAccountImpersonationOptions additional options for service account impersonation, - * may be null. - */ - protected ExternalAccountCredentials( - HttpTransportFactory transportFactory, - String audience, - String subjectTokenType, - String tokenUrl, - CredentialSource credentialSource, - @Nullable String tokenInfoUrl, - @Nullable String serviceAccountImpersonationUrl, - @Nullable String quotaProjectId, - @Nullable String clientId, - @Nullable String clientSecret, - @Nullable Collection scopes, - @Nullable EnvironmentProvider environmentProvider, - @Nullable ServiceAccountImpersonationOptions serviceAccountImpersonationOptions) { this.transportFactory = MoreObjects.firstNonNull( transportFactory, @@ -266,10 +228,6 @@ protected ExternalAccountCredentials( this.credentialSource = checkNotNull(credentialSource); this.tokenInfoUrl = tokenInfoUrl; this.serviceAccountImpersonationUrl = serviceAccountImpersonationUrl; - this.serviceAccountImpersonationOptions = - serviceAccountImpersonationOptions == null - ? new ServiceAccountImpersonationOptions(new HashMap()) - : serviceAccountImpersonationOptions; this.quotaProjectId = quotaProjectId; this.clientId = clientId; this.clientSecret = clientSecret; @@ -278,6 +236,8 @@ protected ExternalAccountCredentials( this.environmentProvider = environmentProvider == null ? SystemEnvironmentProvider.getInstance() : environmentProvider; this.workforcePoolUserProject = null; + this.serviceAccountImpersonationOptions = + new ServiceAccountImpersonationOptions(new HashMap()); validateTokenUrl(tokenUrl); if (serviceAccountImpersonationUrl != null) { @@ -844,6 +804,7 @@ Builder setEnvironmentProvider(EnvironmentProvider environmentProvider) { return this; } + /** Sets the optional service account impersonation options. */ Builder setServiceAccountImpersonationOptions( ServiceAccountImpersonationOptions serviceAccountImpersonationOptions) { this.serviceAccountImpersonationOptions = serviceAccountImpersonationOptions; diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java index 942f6183f..1cc79e371 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java @@ -42,6 +42,7 @@ import com.google.api.client.testing.http.MockLowLevelHttpRequest; import com.google.auth.TestUtils; import com.google.auth.oauth2.AwsCredentials.AwsCredentialSource; +import com.google.auth.oauth2.ExternalAccountCredentials.ServiceAccountImpersonationOptions; import com.google.auth.oauth2.ExternalAccountCredentialsTest.MockExternalAccountCredentialsTransportFactory; import java.io.IOException; import java.io.InputStream; @@ -142,6 +143,41 @@ public void refreshAccessToken_withServiceAccountImpersonation() throws IOExcept transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue()); } + @Test + public void refreshAccessToken_withServiceAccountImpersonationOptions() throws IOException { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); + + AwsCredentials awsCredential = + (AwsCredentials) + AwsCredentials.newBuilder(AWS_CREDENTIAL) + .setTokenUrl(transportFactory.transport.getStsUrl()) + .setServiceAccountImpersonationUrl( + transportFactory.transport.getServiceAccountImpersonationUrl()) + .setHttpTransportFactory(transportFactory) + .setCredentialSource(buildAwsCredentialSource(transportFactory)) + .setServiceAccountImpersonationOptions( + new ServiceAccountImpersonationOptions( + ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions( + 2800))) + .build(); + + AccessToken accessToken = awsCredential.refreshAccessToken(); + + assertEquals( + transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue()); + + // Validate that default lifetime was set correctly on the request. + GenericJson query = + OAuth2Utils.JSON_FACTORY + .createJsonParser(transportFactory.transport.getLastRequest().getContentAsString()) + .parseAndClose(GenericJson.class); + + assertEquals("2800s", query.get("lifetime")); + } + @Test public void retrieveSubjectToken() throws IOException { MockExternalAccountCredentialsTransportFactory transportFactory = diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index 7b868f7c4..f99e5b1cc 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -957,7 +957,7 @@ private GenericJson buildJsonPluggableAuthWorkforceCredential() { return json; } - private Map buildServiceAccountImpersonationOptions(Integer lifetime) { + static Map buildServiceAccountImpersonationOptions(Integer lifetime) { Map map = new HashMap(); map.put("token_lifetime_seconds", lifetime); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java index 0079e6192..9b4e9760b 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ITWorkloadIdentityFederationTest.java @@ -175,7 +175,7 @@ public void pluggableAuthCredentials() throws IOException { * for token_lifetime_seconds and validates that the lifetime is used for the access token. */ @Test - public void IdentityPoolCredentialsWithServiceAccountImpersonationOptions() throws IOException { + public void identityPoolCredentials_withServiceAccountImpersonationOptions() throws IOException { GenericJson identityPoolCredentialConfig = buildIdentityPoolCredentialConfig(); Map map = new HashMap(); map.put("token_lifetime_seconds", 2800); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java index 5efbb3b36..9d31be72f 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java @@ -42,6 +42,7 @@ import com.google.api.client.json.GenericJson; import com.google.auth.TestUtils; import com.google.auth.http.HttpTransportFactory; +import com.google.auth.oauth2.ExternalAccountCredentials.ServiceAccountImpersonationOptions; import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource; import java.io.ByteArrayInputStream; import java.io.File; @@ -386,6 +387,41 @@ public void refreshAccessToken_withServiceAccountImpersonation() throws IOExcept transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue()); } + @Test + public void refreshAccessToken_withServiceAccountImpersonationOptions() throws IOException { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); + IdentityPoolCredentials credential = + (IdentityPoolCredentials) + IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) + .setTokenUrl(transportFactory.transport.getStsUrl()) + .setServiceAccountImpersonationUrl( + transportFactory.transport.getServiceAccountImpersonationUrl()) + .setHttpTransportFactory(transportFactory) + .setCredentialSource( + buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) + .setServiceAccountImpersonationOptions( + new ServiceAccountImpersonationOptions( + ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions( + 2800))) + .build(); + + AccessToken accessToken = credential.refreshAccessToken(); + + assertEquals( + transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue()); + + // Validate that default lifetime was set correctly on the request. + GenericJson query = + OAuth2Utils.JSON_FACTORY + .createJsonParser(transportFactory.transport.getLastRequest().getContentAsString()) + .parseAndClose(GenericJson.class); + + assertEquals("2800s", query.get("lifetime")); + } + @Test public void refreshAccessToken_workforceWithServiceAccountImpersonation() throws IOException { MockExternalAccountCredentialsTransportFactory transportFactory = @@ -422,6 +458,45 @@ public void refreshAccessToken_workforceWithServiceAccountImpersonation() throws assertEquals(expectedInternalOptions.toString(), query.get("options")); } + @Test + public void refreshAccessToken_workforceWithServiceAccountImpersonationOptions() + throws IOException { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); + IdentityPoolCredentials credential = + (IdentityPoolCredentials) + IdentityPoolCredentials.newBuilder(FILE_SOURCED_CREDENTIAL) + .setAudience( + "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") + .setTokenUrl(transportFactory.transport.getStsUrl()) + .setServiceAccountImpersonationUrl( + transportFactory.transport.getServiceAccountImpersonationUrl()) + .setHttpTransportFactory(transportFactory) + .setCredentialSource( + buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) + .setWorkforcePoolUserProject("userProject") + .setServiceAccountImpersonationOptions( + new ServiceAccountImpersonationOptions( + ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions( + 2800))) + .build(); + + AccessToken accessToken = credential.refreshAccessToken(); + + // Validate that default lifetime was set correctly on the request. + assertEquals( + transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue()); + + GenericJson query = + OAuth2Utils.JSON_FACTORY + .createJsonParser(transportFactory.transport.getLastRequest().getContentAsString()) + .parseAndClose(GenericJson.class); + + assertEquals("2800s", query.get("lifetime")); + } + @Test public void identityPoolCredentialSource_validFormats() { Map credentialSourceMapWithFileTextSource = new HashMap<>(); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java index bddccd6e2..15af8314c 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java @@ -42,6 +42,7 @@ import com.google.auth.http.HttpTransportFactory; import com.google.auth.oauth2.ExecutableHandler.ExecutableOptions; import com.google.auth.oauth2.ExternalAccountCredentials.CredentialSource; +import com.google.auth.oauth2.ExternalAccountCredentials.ServiceAccountImpersonationOptions; import com.google.auth.oauth2.PluggableAuthCredentials.PluggableAuthCredentialSource; import java.io.IOException; import java.io.InputStream; @@ -231,6 +232,41 @@ public void refreshAccessToken_withServiceAccountImpersonation() throws IOExcept assertEquals(query.get("subject_token"), "pluggableAuthToken"); } + @Test + public void refreshAccessToken_withServiceAccountImpersonationOptions() throws IOException { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); + + PluggableAuthCredentials credential = + (PluggableAuthCredentials) + PluggableAuthCredentials.newBuilder(CREDENTIAL) + .setExecutableHandler(options -> "pluggableAuthToken") + .setTokenUrl(transportFactory.transport.getStsUrl()) + .setServiceAccountImpersonationUrl( + transportFactory.transport.getServiceAccountImpersonationUrl()) + .setHttpTransportFactory(transportFactory) + .setServiceAccountImpersonationOptions( + new ServiceAccountImpersonationOptions( + ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions( + 2800))) + .build(); + + AccessToken accessToken = credential.refreshAccessToken(); + + assertEquals( + transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue()); + + // Validate that default lifetime was set correctly on the request. + GenericJson query = + OAuth2Utils.JSON_FACTORY + .createJsonParser(transportFactory.transport.getLastRequest().getContentAsString()) + .parseAndClose(GenericJson.class); + + assertEquals("2800s", query.get("lifetime")); + } + @Test public void pluggableAuthCredentialSource_allFields() { Map source = new HashMap<>(); From f29c121f30daff6dc917d58a0333f1f35b50d570 Mon Sep 17 00:00:00 2001 From: aeitzman Date: Mon, 22 Aug 2022 16:32:22 -0700 Subject: [PATCH 4/8] Adding readme documentation --- README.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/README.md b/README.md index 762929749..5a55bd8f2 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ credentials as well as utility methods to create them and to get Application Def * [Accessing resources from Azure](#access-resources-from-microsoft-azure) * [Accessing resources from an OIDC identity provider](#accessing-resources-from-an-oidc-identity-provider) * [Accessing resources using Executable-sourced credentials](#using-executable-sourced-credentials-with-oidc-and-saml) + * [Configurable Token Lifetime](#configurable-token-lifetime) * [Workforce Identity Federation](#workforce-identity-federation) * [Accessing resources using an OIDC or SAML 2.0 identity provider](#accessing-resources-using-an-oidc-or-saml-20-identity-provider) * [Accessing resources using Executable-sourced credentials](#using-executable-sourced-workforce-credentials-with-oidc-and-saml) @@ -467,6 +468,34 @@ credentials unless they do not meet your specific requirements. You can now [use the Auth library](#using-external-identities) to call Google Cloud resources from an OIDC or SAML provider. +#### Configurable Token Lifetime +When creating a credential configuration with workload identity federation using service account impersonation, you can provide an optional argument to configure the access token lifetime. + +To generate the configuration with configurable token lifetime, run the following command (This example uses an AWS configuration, but the token lifetime can be configured for all workload identity federation providers): + ```bash + # Generate an AWS configuration file with configurable token lifetime. + gcloud iam workload-identity-pools create-cred-config \ + projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$AWS_PROVIDER_ID \ + --service-account $SERVICE_ACCOUNT_EMAIL \ + --aws \ + --output-file /path/to/generated/config.json \ + --service-account-token-lifetime-seconds $TOKEN_LIFETIME + ``` + +Where the following variables need to be substituted: +- `$PROJECT_NUMBER`: The Google Cloud project number. +- `$POOL_ID`: The workload identity pool ID. +- `$AWS_PROVIDER_ID`: The AWS provider ID. +- `$SERVICE_ACCOUNT_EMAIL`: The email of the service account to impersonate. +- `$TOKEN_LIFETIME`: The desired lifetime duration of the service account access token in seconds. + +The `service-account-token-lifetime-seconds` flag is optional. If not provided, this defaults to one hour. +The minimum allowed value is 600 (10 minutes) and the maximum allowed value is 43200 (12 hours). +If a lifetime greater than one hour is required, the service account must be added as an allowed value in an Organization Policy that enforces the `constraints/iam.allowServiceAccountCredentialLifetimeExtension` constraint. +Note that configuring a short lifetime (e.g. 10 minutes) will result in the library initiating the entire token exchange flow every 10 minutes, which will call the 3rd party token provider even if the 3rd party token is not expired. + + + ### Workforce Identity Federation [Workforce identity federation](https://cloud.google.com/iam/docs/workforce-identity-federation) lets you use an From 99590a69a3180fb683f541b12d349bc3192624fe Mon Sep 17 00:00:00 2001 From: aeitzman Date: Tue, 23 Aug 2022 11:07:52 -0700 Subject: [PATCH 5/8] fix: addressing code review --- README.md | 7 +++---- .../oauth2/ExternalAccountCredentials.java | 20 +++++++++++-------- .../ExternalAccountCredentialsTest.java | 6 +++--- 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 5a55bd8f2..c94afa4bb 100644 --- a/README.md +++ b/README.md @@ -469,9 +469,9 @@ You can now [use the Auth library](#using-external-identities) to call Google Cl resources from an OIDC or SAML provider. #### Configurable Token Lifetime -When creating a credential configuration with workload identity federation using service account impersonation, you can provide an optional argument to configure the access token lifetime. +When creating a credential configuration with workload identity federation using service account impersonation, you can provide an optional argument to configure the service account access token lifetime. -To generate the configuration with configurable token lifetime, run the following command (This example uses an AWS configuration, but the token lifetime can be configured for all workload identity federation providers): +To generate the configuration with configurable token lifetime, run the following command (this example uses an AWS configuration, but the token lifetime can be configured for all workload identity federation providers): ```bash # Generate an AWS configuration file with configurable token lifetime. gcloud iam workload-identity-pools create-cred-config \ @@ -492,9 +492,8 @@ Where the following variables need to be substituted: The `service-account-token-lifetime-seconds` flag is optional. If not provided, this defaults to one hour. The minimum allowed value is 600 (10 minutes) and the maximum allowed value is 43200 (12 hours). If a lifetime greater than one hour is required, the service account must be added as an allowed value in an Organization Policy that enforces the `constraints/iam.allowServiceAccountCredentialLifetimeExtension` constraint. -Note that configuring a short lifetime (e.g. 10 minutes) will result in the library initiating the entire token exchange flow every 10 minutes, which will call the 3rd party token provider even if the 3rd party token is not expired. - +Note that configuring a short lifetime (e.g. 10 minutes) will result in the library initiating the entire token exchange flow every 10 minutes, which will call the 3rd party token provider even if the 3rd party token is not expired. ### Workforce Identity Federation diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index f178608d5..ebb9be697 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -91,11 +91,11 @@ abstract static class CredentialSource { * } *
*/ - static class ServiceAccountImpersonationOptions { + static final class ServiceAccountImpersonationOptions { private static final int DEFAULT_TOKEN_LIFETIME_SECONDS = 3600; private static final String TOKEN_LIFETIME_SECONDS_KEY = "token_lifetime_seconds"; - public final int lifetime; + private final int lifetime; ServiceAccountImpersonationOptions(Map optionsMap) { if (!optionsMap.containsKey(TOKEN_LIFETIME_SECONDS_KEY)) { @@ -112,6 +112,10 @@ static class ServiceAccountImpersonationOptions { lifetime = Integer.parseInt((String) timeout); } } + + int getLifetime() { + return lifetime; + } } private static final String CLOUD_PLATFORM_SCOPE = @@ -799,18 +803,18 @@ public Builder setWorkforcePoolUserProject(String workforcePoolUserProject) { return this; } - Builder setEnvironmentProvider(EnvironmentProvider environmentProvider) { - this.environmentProvider = environmentProvider; - return this; - } - /** Sets the optional service account impersonation options. */ - Builder setServiceAccountImpersonationOptions( + public Builder setServiceAccountImpersonationOptions( ServiceAccountImpersonationOptions serviceAccountImpersonationOptions) { this.serviceAccountImpersonationOptions = serviceAccountImpersonationOptions; return this; } + Builder setEnvironmentProvider(EnvironmentProvider environmentProvider) { + this.environmentProvider = environmentProvider; + return this; + } + public abstract ExternalAccountCredentials build(); } } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index f99e5b1cc..3d2ea18ec 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -220,7 +220,7 @@ public void fromJson_identityPoolCredentialsWithServiceAccountImpersonationOptio assertEquals(STS_URL, credential.getTokenUrl()); assertEquals("tokenInfoUrl", credential.getTokenInfoUrl()); assertNotNull(credential.getCredentialSource()); - assertEquals(2800, credential.getServiceAccountImpersonationOptions().lifetime); + assertEquals(2800, credential.getServiceAccountImpersonationOptions().getLifetime()); } @Test @@ -252,7 +252,7 @@ public void fromJson_awsCredentialsWithServiceAccountImpersonationOptions() thro assertEquals(STS_URL, credential.getTokenUrl()); assertEquals("tokenInfoUrl", credential.getTokenInfoUrl()); assertNotNull(credential.getCredentialSource()); - assertEquals(2800, credential.getServiceAccountImpersonationOptions().lifetime); + assertEquals(2800, credential.getServiceAccountImpersonationOptions().getLifetime()); } @Test @@ -342,7 +342,7 @@ public void fromJson_pluggableAuthCredentialsWithServiceAccountImpersonation() { assertEquals(STS_URL, credential.getTokenUrl()); assertEquals("tokenInfoUrl", credential.getTokenInfoUrl()); assertNotNull(credential.getCredentialSource()); - assertEquals(2800, credential.getServiceAccountImpersonationOptions().lifetime); + assertEquals(2800, credential.getServiceAccountImpersonationOptions().getLifetime()); PluggableAuthCredentialSource source = (PluggableAuthCredentialSource) credential.getCredentialSource(); From 9f875197fcda9cd29669e068cae73f1aaa394b7c Mon Sep 17 00:00:00 2001 From: aeitzman Date: Tue, 23 Aug 2022 15:04:11 -0700 Subject: [PATCH 6/8] fix: make impersonation options object public --- .../java/com/google/auth/oauth2/ExternalAccountCredentials.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index ebb9be697..49946f8e5 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -91,7 +91,7 @@ abstract static class CredentialSource { * } * */ - static final class ServiceAccountImpersonationOptions { + public static final class ServiceAccountImpersonationOptions { private static final int DEFAULT_TOKEN_LIFETIME_SECONDS = 3600; private static final String TOKEN_LIFETIME_SECONDS_KEY = "token_lifetime_seconds"; From 01a219ce9a21387ac67834fa01db71f1d80b88b1 Mon Sep 17 00:00:00 2001 From: aeitzman Date: Wed, 24 Aug 2022 14:55:13 -0700 Subject: [PATCH 7/8] fix: addressing code review comments --- .../oauth2/ExternalAccountCredentials.java | 108 ++++++++--------- .../auth/oauth2/AwsCredentialsTest.java | 5 +- .../ExternalAccountCredentialsTest.java | 110 ++++++++++++++++++ .../oauth2/IdentityPoolCredentialsTest.java | 9 +- .../oauth2/PluggableAuthCredentialsTest.java | 5 +- 5 files changed, 169 insertions(+), 68 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index 49946f8e5..3c7eebf66 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -75,49 +75,6 @@ abstract static class CredentialSource { } } - /** - * Encapsulates the service account impersonation options portion of the configuration for - * ExternalAccountCredentials. - * - *

If token_lifetime_seconds is not specified, the library will default to a 1-hour lifetime. - * - *

-   * Sample configuration:
-   * {
-   *   ...
-   *   "service_account_impersonation": {
-   *     "token_lifetime_seconds": 2800
-   *    }
-   * }
-   * 
- */ - public static final class ServiceAccountImpersonationOptions { - private static final int DEFAULT_TOKEN_LIFETIME_SECONDS = 3600; - private static final String TOKEN_LIFETIME_SECONDS_KEY = "token_lifetime_seconds"; - - private final int lifetime; - - ServiceAccountImpersonationOptions(Map optionsMap) { - if (!optionsMap.containsKey(TOKEN_LIFETIME_SECONDS_KEY)) { - lifetime = DEFAULT_TOKEN_LIFETIME_SECONDS; - return; - } - - Object timeout = optionsMap.get(TOKEN_LIFETIME_SECONDS_KEY); - if (timeout instanceof BigDecimal) { - lifetime = ((BigDecimal) timeout).intValue(); - } else if (optionsMap.get(TOKEN_LIFETIME_SECONDS_KEY) instanceof Integer) { - lifetime = (int) timeout; - } else { - lifetime = Integer.parseInt((String) timeout); - } - } - - int getLifetime() { - return lifetime; - } - } - private static final String CLOUD_PLATFORM_SCOPE = "https://www.googleapis.com/auth/cloud-platform"; @@ -430,10 +387,8 @@ static ExternalAccountCredentials fromJson( Map impersonationOptionsMap = (Map) json.get("service_account_impersonation"); - ServiceAccountImpersonationOptions serviceAccountImpersonationOptions = null; - if (impersonationOptionsMap != null) { - serviceAccountImpersonationOptions = - new ServiceAccountImpersonationOptions(impersonationOptionsMap); + if (impersonationOptionsMap == null) { + impersonationOptionsMap = new HashMap(); } if (isAwsCredential(credentialSourceMap)) { @@ -448,7 +403,7 @@ static ExternalAccountCredentials fromJson( .setQuotaProjectId(quotaProjectId) .setClientId(clientId) .setClientSecret(clientSecret) - .setServiceAccountImpersonationOptions(serviceAccountImpersonationOptions) + .setServiceAccountImpersonationOptions(impersonationOptionsMap) .build(); } else if (isPluggableAuthCredential(credentialSourceMap)) { return PluggableAuthCredentials.newBuilder() @@ -463,7 +418,7 @@ static ExternalAccountCredentials fromJson( .setClientId(clientId) .setClientSecret(clientSecret) .setWorkforcePoolUserProject(userProject) - .setServiceAccountImpersonationOptions(serviceAccountImpersonationOptions) + .setServiceAccountImpersonationOptions(impersonationOptionsMap) .build(); } return IdentityPoolCredentials.newBuilder() @@ -478,7 +433,7 @@ static ExternalAccountCredentials fromJson( .setClientId(clientId) .setClientSecret(clientSecret) .setWorkforcePoolUserProject(userProject) - .setServiceAccountImpersonationOptions(serviceAccountImpersonationOptions) + .setServiceAccountImpersonationOptions(impersonationOptionsMap) .build(); } @@ -676,6 +631,54 @@ private static boolean isValidUrl(List patterns, String url) { return false; } + /** + * Encapsulates the service account impersonation options portion of the configuration for + * ExternalAccountCredentials. + * + *

If token_lifetime_seconds is not specified, the library will default to a 1-hour lifetime. + * + *

+   * Sample configuration:
+   * {
+   *   ...
+   *   "service_account_impersonation": {
+   *     "token_lifetime_seconds": 2800
+   *    }
+   * }
+   * 
+ */ + static final class ServiceAccountImpersonationOptions { + private static final int DEFAULT_TOKEN_LIFETIME_SECONDS = 3600; + private static final String TOKEN_LIFETIME_SECONDS_KEY = "token_lifetime_seconds"; + + private final int lifetime; + + ServiceAccountImpersonationOptions(Map optionsMap) { + if (!optionsMap.containsKey(TOKEN_LIFETIME_SECONDS_KEY)) { + lifetime = DEFAULT_TOKEN_LIFETIME_SECONDS; + return; + } + + try { + Object lifetimeValue = optionsMap.get(TOKEN_LIFETIME_SECONDS_KEY); + if (lifetimeValue instanceof BigDecimal) { + lifetime = ((BigDecimal) lifetimeValue).intValue(); + } else if (optionsMap.get(TOKEN_LIFETIME_SECONDS_KEY) instanceof Integer) { + lifetime = (int) lifetimeValue; + } else { + lifetime = Integer.parseInt((String) lifetimeValue); + } + } catch (NumberFormatException | ArithmeticException e) { + throw new IllegalArgumentException( + "Value of \"token_lifetime_seconds\" field could not be parsed into an integer.", e); + } + } + + int getLifetime() { + return lifetime; + } + } + /** Base builder for external account credentials. */ public abstract static class Builder extends GoogleCredentials.Builder { @@ -804,9 +807,8 @@ public Builder setWorkforcePoolUserProject(String workforcePoolUserProject) { } /** Sets the optional service account impersonation options. */ - public Builder setServiceAccountImpersonationOptions( - ServiceAccountImpersonationOptions serviceAccountImpersonationOptions) { - this.serviceAccountImpersonationOptions = serviceAccountImpersonationOptions; + public Builder setServiceAccountImpersonationOptions(Map optionsMap) { + this.serviceAccountImpersonationOptions = new ServiceAccountImpersonationOptions(optionsMap); return this; } diff --git a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java index 1cc79e371..9b67a09bb 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java @@ -42,7 +42,6 @@ import com.google.api.client.testing.http.MockLowLevelHttpRequest; import com.google.auth.TestUtils; import com.google.auth.oauth2.AwsCredentials.AwsCredentialSource; -import com.google.auth.oauth2.ExternalAccountCredentials.ServiceAccountImpersonationOptions; import com.google.auth.oauth2.ExternalAccountCredentialsTest.MockExternalAccountCredentialsTransportFactory; import java.io.IOException; import java.io.InputStream; @@ -159,9 +158,7 @@ public void refreshAccessToken_withServiceAccountImpersonationOptions() throws I .setHttpTransportFactory(transportFactory) .setCredentialSource(buildAwsCredentialSource(transportFactory)) .setServiceAccountImpersonationOptions( - new ServiceAccountImpersonationOptions( - ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions( - 2800))) + ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions(2800)) .build(); AccessToken accessToken = awsCredential.refreshAccessToken(); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index 3d2ea18ec..8e690d3a6 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -46,6 +46,7 @@ import com.google.auth.oauth2.PluggableAuthCredentials.PluggableAuthCredentialSource; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.math.BigDecimal; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; @@ -539,6 +540,115 @@ public void constructor_builderWithEmptyWorkforceUserProjectAndWorkforceAudience .build(); } + @Test + public void constructor_builder_invalidTokenLifetime() { + Map invalidOptionsMap = new HashMap(); + invalidOptionsMap.put("token_lifetime_seconds", "thisIsAString"); + + try { + IdentityPoolCredentials.newBuilder() + .setHttpTransportFactory(transportFactory) + .setAudience( + "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("https://tokeninfo.com") + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP)) + .setScopes(Arrays.asList("scope1", "scope2")) + .setQuotaProjectId("projectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setWorkforcePoolUserProject("workforcePoolUserProject") + .setServiceAccountImpersonationOptions(invalidOptionsMap) + .build(); + fail("Should not be able to continue without exception."); + } catch (IllegalArgumentException exception) { + assertEquals( + "Value of \"token_lifetime_seconds\" field could not be parsed into an integer.", + exception.getMessage()); + assertEquals(NumberFormatException.class, exception.getCause().getClass()); + } + } + + @Test + public void constructor_builder_stringTokenLifetime() { + Map optionsMap = new HashMap(); + optionsMap.put("token_lifetime_seconds", "2800"); + + ExternalAccountCredentials credentials = + IdentityPoolCredentials.newBuilder() + .setHttpTransportFactory(transportFactory) + .setAudience( + "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("https://tokeninfo.com") + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP)) + .setScopes(Arrays.asList("scope1", "scope2")) + .setQuotaProjectId("projectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setWorkforcePoolUserProject("workforcePoolUserProject") + .setServiceAccountImpersonationOptions(optionsMap) + .build(); + + assertEquals(2800, credentials.getServiceAccountImpersonationOptions().getLifetime()); + } + + @Test + public void constructor_builder_bigDecimalTokenLifetime() { + Map optionsMap = new HashMap(); + optionsMap.put("token_lifetime_seconds", new BigDecimal("2800")); + + ExternalAccountCredentials credentials = + IdentityPoolCredentials.newBuilder() + .setHttpTransportFactory(transportFactory) + .setAudience( + "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("https://tokeninfo.com") + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP)) + .setScopes(Arrays.asList("scope1", "scope2")) + .setQuotaProjectId("projectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setWorkforcePoolUserProject("workforcePoolUserProject") + .setServiceAccountImpersonationOptions(optionsMap) + .build(); + + assertEquals(2800, credentials.getServiceAccountImpersonationOptions().getLifetime()); + } + + @Test + public void constructor_builder_integerTokenLifetime() { + Map optionsMap = new HashMap(); + optionsMap.put("token_lifetime_seconds", Integer.valueOf(2800)); + + ExternalAccountCredentials credentials = + IdentityPoolCredentials.newBuilder() + .setHttpTransportFactory(transportFactory) + .setAudience( + "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("https://tokeninfo.com") + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP)) + .setScopes(Arrays.asList("scope1", "scope2")) + .setQuotaProjectId("projectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setWorkforcePoolUserProject("workforcePoolUserProject") + .setServiceAccountImpersonationOptions(optionsMap) + .build(); + + assertEquals(2800, credentials.getServiceAccountImpersonationOptions().getLifetime()); + } + @Test public void exchangeExternalCredentialForAccessToken() throws IOException { ExternalAccountCredentials credential = diff --git a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java index 9d31be72f..373f9b3fb 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java @@ -42,7 +42,6 @@ import com.google.api.client.json.GenericJson; import com.google.auth.TestUtils; import com.google.auth.http.HttpTransportFactory; -import com.google.auth.oauth2.ExternalAccountCredentials.ServiceAccountImpersonationOptions; import com.google.auth.oauth2.IdentityPoolCredentials.IdentityPoolCredentialSource; import java.io.ByteArrayInputStream; import java.io.File; @@ -403,9 +402,7 @@ public void refreshAccessToken_withServiceAccountImpersonationOptions() throws I .setCredentialSource( buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) .setServiceAccountImpersonationOptions( - new ServiceAccountImpersonationOptions( - ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions( - 2800))) + ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions(2800)) .build(); AccessToken accessToken = credential.refreshAccessToken(); @@ -478,9 +475,7 @@ public void refreshAccessToken_workforceWithServiceAccountImpersonationOptions() buildUrlBasedCredentialSource(transportFactory.transport.getMetadataUrl())) .setWorkforcePoolUserProject("userProject") .setServiceAccountImpersonationOptions( - new ServiceAccountImpersonationOptions( - ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions( - 2800))) + ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions(2800)) .build(); AccessToken accessToken = credential.refreshAccessToken(); diff --git a/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java index 15af8314c..cf316ade1 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java @@ -42,7 +42,6 @@ import com.google.auth.http.HttpTransportFactory; import com.google.auth.oauth2.ExecutableHandler.ExecutableOptions; import com.google.auth.oauth2.ExternalAccountCredentials.CredentialSource; -import com.google.auth.oauth2.ExternalAccountCredentials.ServiceAccountImpersonationOptions; import com.google.auth.oauth2.PluggableAuthCredentials.PluggableAuthCredentialSource; import java.io.IOException; import java.io.InputStream; @@ -248,9 +247,7 @@ public void refreshAccessToken_withServiceAccountImpersonationOptions() throws I transportFactory.transport.getServiceAccountImpersonationUrl()) .setHttpTransportFactory(transportFactory) .setServiceAccountImpersonationOptions( - new ServiceAccountImpersonationOptions( - ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions( - 2800))) + ExternalAccountCredentialsTest.buildServiceAccountImpersonationOptions(2800)) .build(); AccessToken accessToken = credential.refreshAccessToken(); From 2f37d10c7f83766b84e17c7328cc905e5e4bbcbb Mon Sep 17 00:00:00 2001 From: aeitzman Date: Fri, 2 Sep 2022 11:05:15 -0700 Subject: [PATCH 8/8] Add check for lifetime min and max value --- .../oauth2/ExternalAccountCredentials.java | 9 +++ .../ExternalAccountCredentialsTest.java | 64 ++++++++++++++++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index 3c7eebf66..b718d3a7a 100644 --- a/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -649,6 +649,8 @@ private static boolean isValidUrl(List patterns, String url) { */ static final class ServiceAccountImpersonationOptions { private static final int DEFAULT_TOKEN_LIFETIME_SECONDS = 3600; + private static final int MAXIMUM_TOKEN_LIFETIME_SECONDS = 43200; + private static final int MINIMUM_TOKEN_LIFETIME_SECONDS = 600; private static final String TOKEN_LIFETIME_SECONDS_KEY = "token_lifetime_seconds"; private final int lifetime; @@ -672,6 +674,13 @@ static final class ServiceAccountImpersonationOptions { throw new IllegalArgumentException( "Value of \"token_lifetime_seconds\" field could not be parsed into an integer.", e); } + + if (lifetime < MINIMUM_TOKEN_LIFETIME_SECONDS || lifetime > MAXIMUM_TOKEN_LIFETIME_SECONDS) { + throw new IllegalArgumentException( + String.format( + "The \"token_lifetime_seconds\" field must be between %s and %s seconds.", + MINIMUM_TOKEN_LIFETIME_SECONDS, MAXIMUM_TOKEN_LIFETIME_SECONDS)); + } } int getLifetime() { diff --git a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java index 8e690d3a6..5d7d188a2 100644 --- a/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java +++ b/oauth2_http/javatests/com/google/auth/oauth2/ExternalAccountCredentialsTest.java @@ -328,7 +328,7 @@ public void fromJson_pluggableAuthCredentials_allExecutableOptionsSet() { } @Test - public void fromJson_pluggableAuthCredentialsWithServiceAccountImpersonation() { + public void fromJson_pluggableAuthCredentialsWithServiceAccountImpersonationOptions() { GenericJson pluggableAuthCredentialJson = buildJsonPluggableAuthCredential(); pluggableAuthCredentialJson.set( "service_account_impersonation", buildServiceAccountImpersonationOptions(2800)); @@ -541,7 +541,7 @@ public void constructor_builderWithEmptyWorkforceUserProjectAndWorkforceAudience } @Test - public void constructor_builder_invalidTokenLifetime() { + public void constructor_builder_invalidTokenLifetime_throws() { Map invalidOptionsMap = new HashMap(); invalidOptionsMap.put("token_lifetime_seconds", "thisIsAString"); @@ -649,6 +649,66 @@ public void constructor_builder_integerTokenLifetime() { assertEquals(2800, credentials.getServiceAccountImpersonationOptions().getLifetime()); } + @Test + public void constructor_builder_lowTokenLifetime_throws() { + Map optionsMap = new HashMap(); + optionsMap.put("token_lifetime_seconds", 599); + + try { + ExternalAccountCredentials credentials = + IdentityPoolCredentials.newBuilder() + .setHttpTransportFactory(transportFactory) + .setAudience( + "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("https://tokeninfo.com") + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP)) + .setScopes(Arrays.asList("scope1", "scope2")) + .setQuotaProjectId("projectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setWorkforcePoolUserProject("workforcePoolUserProject") + .setServiceAccountImpersonationOptions(optionsMap) + .build(); + } catch (IllegalArgumentException e) { + assertEquals( + "The \"token_lifetime_seconds\" field must be between 600 and 43200 seconds.", + e.getMessage()); + } + } + + @Test + public void constructor_builder_highTokenLifetime_throws() { + Map optionsMap = new HashMap(); + optionsMap.put("token_lifetime_seconds", 43201); + + try { + ExternalAccountCredentials credentials = + IdentityPoolCredentials.newBuilder() + .setHttpTransportFactory(transportFactory) + .setAudience( + "//iam.googleapis.com/locations/global/workforcePools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenUrl(STS_URL) + .setTokenInfoUrl("https://tokeninfo.com") + .setServiceAccountImpersonationUrl(SERVICE_ACCOUNT_IMPERSONATION_URL) + .setCredentialSource(new TestCredentialSource(FILE_CREDENTIAL_SOURCE_MAP)) + .setScopes(Arrays.asList("scope1", "scope2")) + .setQuotaProjectId("projectId") + .setClientId("clientId") + .setClientSecret("clientSecret") + .setWorkforcePoolUserProject("workforcePoolUserProject") + .setServiceAccountImpersonationOptions(optionsMap) + .build(); + } catch (IllegalArgumentException e) { + assertEquals( + "The \"token_lifetime_seconds\" field must be between 600 and 43200 seconds.", + e.getMessage()); + } + } + @Test public void exchangeExternalCredentialForAccessToken() throws IOException { ExternalAccountCredentials credential =