diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java index e6846eaee550..b09b1eb9b7e9 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/IdentityPoolCredentials.java @@ -40,7 +40,6 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.io.IOException; import java.io.ObjectInputStream; -import java.net.URI; import java.security.KeyStore; import java.util.ArrayList; import java.util.Collection; @@ -53,9 +52,9 @@ * *
By default, attempts to exchange the external credential for a GCP access token. * - *
Note: Actor token extraction is currently restricted to file-based JSON credential sources - * over mTLS endpoints. When configuring certificate-bound OAuth 2.0 tokens, ensure your transport - * layer is configured for mTLS in tandem. + *
Note: Actor token extraction from credential configuration files is currently restricted to + * file-based JSON credential sources. When configuring certificate-bound OAuth 2.0 tokens, ensure + * your transport layer and endpoints are configured for mTLS in tandem. */ @NullMarked public class IdentityPoolCredentials extends ExternalAccountCredentials { @@ -178,54 +177,6 @@ public class IdentityPoolCredentials extends ExternalAccountCredentials { throw new IllegalArgumentException( "An actorTokenSupplier must be specified when an actorTokenType is configured."); } - - if (this.actorTokenSupplier != null && !isMtlsConfigured()) { - throw new IllegalArgumentException( - "Actor tokens are only supported for mTLS token exchanges. Please configure a certificate" - + " source or MtlsHttpTransportFactory."); - } - - if (this.actorTokenSupplier != null) { - validateMtlsEndpoint(getTokenUrl(), "tokenUrl"); - if (getServiceAccountImpersonationUrl() != null) { - validateMtlsEndpoint(getServiceAccountImpersonationUrl(), "serviceAccountImpersonationUrl"); - } - } - } - - private static void validateMtlsEndpoint(@Nullable String url, String fieldName) { - if (url == null) { - return; - } - String host = URI.create(url).getHost(); - // For Google Default Universe (googleapis.com), actor tokens require mTLS binding so plain - // public endpoints (lacking '.mtls.' or Private Service Connect '.p.') are rejected early. - // Non-GDU domains (e.g. custom universes or TPC) may use different hostname conventions. - if (host != null - && host.endsWith("googleapis.com") - && !host.contains(".mtls.") - && !host.contains(".p.")) { - throw new IllegalArgumentException( - "The " - + fieldName - + " endpoint (" - + url - + ") cannot be used with actor tokens because it is a plain public Google API" - + " endpoint. Please use an mTLS endpoint (e.g. containing '.mtls.') or Private" - + " Service Connect (containing '.p.')."); - } - } - - /** - * Checks whether mTLS is properly configured by verifying that an X509Provider is set or the - * transport factory is an MtlsHttpTransportFactory with a non-null KeyStore. This avoids false - * positives from a no-arg-constructed MtlsHttpTransportFactory (e.g. after deserialization) that - * has no actual certificates. - */ - private boolean isMtlsConfigured() { - return this.x509Provider != null - || (this.transportFactory instanceof MtlsHttpTransportFactory - && ((MtlsHttpTransportFactory) this.transportFactory).hasKeyStore()); } @Override @@ -479,12 +430,11 @@ public Builder setSubjectTokenSupplier(IdentityPoolSubjectTokenSupplier subjectT } /** - * Sets the actor token supplier used for certificate-bound OAuth 2.0 token exchanges. The - * supplier provides an actor token representing the entity on whose behalf the subject is - * acting. + * Sets the actor token supplier used for OAuth 2.0 token exchanges. The supplier provides an + * actor token representing the entity on whose behalf the subject is acting. * - *
An actor token supplier must be paired with an {@link #setActorTokenType actor token type} - * and requires an mTLS-configured transport. + *
An actor token supplier must be paired with an {@link #setActorTokenType actor token
+ * type}.
*
* @param actorTokenSupplier the supplier to use for retrieving actor tokens
* @return this {@code Builder} object
diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
index a081814a9020..8fdf7efd128b 100644
--- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
+++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/IdentityPoolCredentialsTest.java
@@ -1375,33 +1375,30 @@ void setShouldThrowOnGetKeyStore(boolean shouldThrow) {
}
@Test
- void builder_actorTokenWithNonMtlsTransportFactory_throws() {
+ void builder_actorTokenWithNonMtlsTransportFactory_succeeds() {
IdentityPoolCredentialSource credentialSource = createFileCredentialSource();
+ IdentityPoolActorTokenSupplier actorSupplier =
+ new IdentityPoolActorTokenSupplier() {
+ @Override
+ public String getActorToken(ExternalAccountSupplierContext context) {
+ return "token";
+ }
+ };
- IllegalArgumentException e =
- assertThrows(
- IllegalArgumentException.class,
- () ->
- IdentityPoolCredentials.newBuilder()
- .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
- .setAudience("audience")
- .setSubjectTokenType("subjectTokenType")
- .setTokenUrl("https://invalid.googleapis.com/")
- .setCredentialSource(credentialSource)
- .setActorTokenType("actorTokenType")
- .setActorTokenSupplier(
- new IdentityPoolActorTokenSupplier() {
- @Override
- public String getActorToken(ExternalAccountSupplierContext context) {
- return "token";
- }
- })
- .build());
+ IdentityPoolCredentials credentials =
+ IdentityPoolCredentials.newBuilder()
+ .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
+ .setAudience("audience")
+ .setSubjectTokenType("subjectTokenType")
+ .setTokenUrl("https://sts.googleapis.com/v1/token")
+ .setCredentialSource(credentialSource)
+ .setActorTokenType("actorTokenType")
+ .setActorTokenSupplier(actorSupplier)
+ .build();
- assertEquals(
- "Actor tokens are only supported for mTLS token exchanges. Please configure a certificate"
- + " source or MtlsHttpTransportFactory.",
- e.getMessage());
+ assertNotNull(credentials);
+ assertSame(actorSupplier, credentials.getIdentityPoolActorTokenSupplier());
+ assertEquals("actorTokenType", credentials.getActorTokenType());
}
@Test
@@ -1723,70 +1720,64 @@ void toBuilder_preservesActorTokenType() throws Exception {
}
@Test
- void builder_actorTokenWithoutMtls_throws() {
- IllegalArgumentException e =
- assertThrows(
- IllegalArgumentException.class,
- () ->
- IdentityPoolCredentials.newBuilder()
- .setSubjectTokenSupplier(testProvider)
- .setActorTokenSupplier(testActorSupplier)
- .setActorTokenType("urn:ietf:params:oauth:token-type:jwt")
- .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
- .setAudience("audience")
- .setSubjectTokenType("subjectTokenType")
- .setTokenUrl("https://sts.googleapis.com/v1/token")
- .build());
- assertTrue(
- e.getMessage().contains("Actor tokens are only supported for mTLS token exchanges."));
+ void builder_actorTokenWithoutMtls_succeeds() {
+ IdentityPoolCredentials credentials =
+ IdentityPoolCredentials.newBuilder()
+ .setSubjectTokenSupplier(testProvider)
+ .setActorTokenSupplier(testActorSupplier)
+ .setActorTokenType("urn:ietf:params:oauth:token-type:jwt")
+ .setHttpTransportFactory(OAuth2Utils.HTTP_TRANSPORT_FACTORY)
+ .setAudience("audience")
+ .setSubjectTokenType("subjectTokenType")
+ .setTokenUrl("https://sts.googleapis.com/v1/token")
+ .build();
+ assertNotNull(credentials);
+ assertSame(testActorSupplier, credentials.getIdentityPoolActorTokenSupplier());
+ assertEquals("urn:ietf:params:oauth:token-type:jwt", credentials.getActorTokenType());
}
@Test
- void builder_actorTokenWithNoArgMtlsFactory_throws() throws Exception {
- // A no-arg MtlsHttpTransportFactory (e.g. from deserialization) has no KeyStore,
- // so isMtlsConfigured() should return false and building should fail.
+ void builder_actorTokenWithNoArgMtlsFactory_succeeds() throws Exception {
MtlsHttpTransportFactory noArgFactory = new MtlsHttpTransportFactory();
assertFalse(noArgFactory.hasKeyStore());
- IllegalArgumentException e =
- assertThrows(
- IllegalArgumentException.class,
- () ->
- IdentityPoolCredentials.newBuilder()
- .setSubjectTokenSupplier(testProvider)
- .setActorTokenSupplier(testActorSupplier)
- .setActorTokenType("urn:ietf:params:oauth:token-type:jwt")
- .setHttpTransportFactory(noArgFactory)
- .setAudience("audience")
- .setSubjectTokenType("subjectTokenType")
- .setTokenUrl("https://sts.mtls.googleapis.com/v1/token")
- .build());
- assertTrue(
- e.getMessage().contains("Actor tokens are only supported for mTLS token exchanges."));
+ IdentityPoolCredentials credentials =
+ IdentityPoolCredentials.newBuilder()
+ .setSubjectTokenSupplier(testProvider)
+ .setActorTokenSupplier(testActorSupplier)
+ .setActorTokenType("urn:ietf:params:oauth:token-type:jwt")
+ .setHttpTransportFactory(noArgFactory)
+ .setAudience("audience")
+ .setSubjectTokenType("subjectTokenType")
+ .setTokenUrl("https://sts.googleapis.com/v1/token")
+ .build();
+ assertNotNull(credentials);
+ assertSame(testActorSupplier, credentials.getIdentityPoolActorTokenSupplier());
+ assertEquals("urn:ietf:params:oauth:token-type:jwt", credentials.getActorTokenType());
+ assertEquals("https://sts.googleapis.com/v1/token", credentials.getTokenUrl());
}
@Test
- void builder_actorTokenWithEmptyMtlsFactory_throws() throws Exception {
+ void builder_actorTokenWithEmptyMtlsFactory_succeeds() throws Exception {
KeyStore emptyKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
emptyKeyStore.load(null, null);
MtlsHttpTransportFactory emptyFactory = new MtlsHttpTransportFactory(emptyKeyStore);
assertFalse(emptyFactory.hasKeyStore());
- IllegalArgumentException e =
- assertThrows(
- IllegalArgumentException.class,
- () ->
- IdentityPoolCredentials.newBuilder()
- .setSubjectTokenSupplier(testProvider)
- .setActorTokenSupplier(testActorSupplier)
- .setActorTokenType("urn:ietf:params:oauth:token-type:jwt")
- .setHttpTransportFactory(emptyFactory)
- .setAudience("audience")
- .setSubjectTokenType("subjectTokenType")
- .setTokenUrl("https://sts.mtls.googleapis.com/v1/token")
- .build());
- assertTrue(
- e.getMessage().contains("Actor tokens are only supported for mTLS token exchanges."));
+ IdentityPoolCredentials credentials =
+ IdentityPoolCredentials.newBuilder()
+ .setSubjectTokenSupplier(testProvider)
+ .setActorTokenSupplier(testActorSupplier)
+ .setActorTokenType("urn:ietf:params:oauth:token-type:jwt")
+ .setHttpTransportFactory(emptyFactory)
+ .setAudience("audience")
+ .setSubjectTokenType("subjectTokenType")
+ .setTokenUrl("https://sts.googleapis.com/v1/token")
+ .build();
+ assertNotNull(credentials);
+ assertSame(testActorSupplier, credentials.getIdentityPoolActorTokenSupplier());
+ assertEquals("urn:ietf:params:oauth:token-type:jwt", credentials.getActorTokenType());
+ assertEquals("https://sts.googleapis.com/v1/token", credentials.getTokenUrl());
}
@Test
@@ -2697,52 +2688,47 @@ void serialize_deserialize_programmaticFlow_withMtlsTransport_restoresFactoryWit
}
@Test
- void builder_actorTokenWithPlainPublicTokenUrl_throwsIllegalArgumentException() throws Exception {
+ void builder_actorTokenWithPlainPublicTokenUrl_succeeds() throws Exception {
KeyStore keyStore = createPopulatedKeyStore();
HttpTransportFactory transportFactory = new MtlsHttpTransportFactory(keyStore);
- IllegalArgumentException e =
- assertThrows(
- IllegalArgumentException.class,
- () ->
- IdentityPoolCredentials.newBuilder()
- .setSubjectTokenSupplier(context -> "testSubjectToken")
- .setActorTokenSupplier(context -> "testActorToken")
- .setActorTokenType("urn:ietf:params:oauth:token-type:jwt")
- .setAudience(
- "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider")
- .setSubjectTokenType("urn:ietf:params:oauth:token-type:jwt")
- .setTokenUrl("https://sts.googleapis.com/v1/token")
- .setHttpTransportFactory(transportFactory)
- .build());
- assertTrue(e.getMessage().contains("tokenUrl"));
- assertTrue(e.getMessage().contains("plain public Google API endpoint"));
+ IdentityPoolCredentials cred =
+ IdentityPoolCredentials.newBuilder()
+ .setSubjectTokenSupplier(context -> "testSubjectToken")
+ .setActorTokenSupplier(context -> "testActorToken")
+ .setActorTokenType("urn:ietf:params:oauth:token-type:jwt")
+ .setAudience(
+ "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider")
+ .setSubjectTokenType("urn:ietf:params:oauth:token-type:jwt")
+ .setTokenUrl("https://sts.googleapis.com/v1/token")
+ .setHttpTransportFactory(transportFactory)
+ .build();
+ assertNotNull(cred);
+ assertEquals("urn:ietf:params:oauth:token-type:jwt", cred.getActorTokenType());
+ assertEquals("https://sts.googleapis.com/v1/token", cred.getTokenUrl());
}
@Test
- void builder_actorTokenWithPlainPublicImpersonationUrl_throwsIllegalArgumentException()
- throws Exception {
+ void builder_actorTokenWithPlainPublicImpersonationUrl_succeeds() throws Exception {
KeyStore keyStore = createPopulatedKeyStore();
HttpTransportFactory transportFactory = new MtlsHttpTransportFactory(keyStore);
- IllegalArgumentException e =
- assertThrows(
- IllegalArgumentException.class,
- () ->
- IdentityPoolCredentials.newBuilder()
- .setSubjectTokenSupplier(context -> "testSubjectToken")
- .setActorTokenSupplier(context -> "testActorToken")
- .setActorTokenType("urn:ietf:params:oauth:token-type:jwt")
- .setAudience(
- "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider")
- .setSubjectTokenType("urn:ietf:params:oauth:token-type:jwt")
- .setTokenUrl(MockExternalAccountCredentialsTransport.STS_MTLS_URL)
- .setServiceAccountImpersonationUrl(
- "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@project.iam.gserviceaccount.com:generateAccessToken")
- .setHttpTransportFactory(transportFactory)
- .build());
- assertTrue(e.getMessage().contains("serviceAccountImpersonationUrl"));
- assertTrue(e.getMessage().contains("plain public Google API endpoint"));
+ IdentityPoolCredentials cred =
+ IdentityPoolCredentials.newBuilder()
+ .setSubjectTokenSupplier(context -> "testSubjectToken")
+ .setActorTokenSupplier(context -> "testActorToken")
+ .setActorTokenType("urn:ietf:params:oauth:token-type:jwt")
+ .setAudience(
+ "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider")
+ .setSubjectTokenType("urn:ietf:params:oauth:token-type:jwt")
+ .setTokenUrl(MockExternalAccountCredentialsTransport.STS_MTLS_URL)
+ .setServiceAccountImpersonationUrl(
+ "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/test@project.iam.gserviceaccount.com:generateAccessToken")
+ .setHttpTransportFactory(transportFactory)
+ .build();
+ assertNotNull(cred);
+ assertEquals("urn:ietf:params:oauth:token-type:jwt", cred.getActorTokenType());
+ assertEquals(MockExternalAccountCredentialsTransport.STS_MTLS_URL, cred.getTokenUrl());
}
@Test
@@ -3088,6 +3074,107 @@ public KeyStore getKeyStore() {
assertNotSame(originalTransportFactory, scoped.getTransportFactory());
}
+ @Test
+ void refreshAccessToken_401WithActorTokenAndNonMtlsTransport_bubblesUpWithoutRetry() {
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+
+ IdentityPoolCredentials.Builder builder =
+ IdentityPoolCredentials.newBuilder()
+ .setSubjectTokenSupplier(context -> "testSubjectToken")
+ .setActorTokenSupplier(context -> "testActorToken")
+ .setActorTokenType("urn:ietf:params:oauth:token-type:jwt")
+ .setAudience(
+ "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/p/providers/pr")
+ .setSubjectTokenType("urn:ietf:params:oauth:token-type:jwt")
+ .setTokenUrl("https://sts.googleapis.com/v1/token")
+ .setHttpTransportFactory(transportFactory);
+
+ TestableIdentityPoolCredentials testable =
+ new TestableIdentityPoolCredentials(builder, true, false);
+
+ OAuthException thrown = assertThrows(OAuthException.class, testable::refreshAccessToken);
+ assertEquals(401, thrown.getHttpStatusCode());
+ assertEquals(0, thrown.getSuppressed().length);
+ assertEquals(1, testable.getExchangeCallCount());
+ }
+
+ @Test
+ void
+ fromStream_fileCredentialSource_withoutCertificateConfig_andActorToken_withNonMtlsUrl_refreshesSuccessfully(
+ @TempDir Path tempDir) throws Exception {
+ MockExternalAccountCredentialsTransportFactory transportFactory =
+ new MockExternalAccountCredentialsTransportFactory();
+
+ Path tokenFile = tempDir.resolve("credential_non_mtls.json");
+ GenericJson tokenJson = new GenericJson();
+ tokenJson.setFactory(JSON_FACTORY);
+ tokenJson.put("subject_token", "nonMtlsSubjectToken");
+ tokenJson.put("actor_token", "nonMtlsActorToken");
+ OAuth2Utils.writeInputStreamToFile(
+ new ByteArrayInputStream(tokenJson.toPrettyString().getBytes(StandardCharsets.UTF_8)),
+ tokenFile.toString());
+
+ String configJson =
+ "{\n"
+ + " \"type\": \"external_account\",\n"
+ + " \"audience\":"
+ + " \"//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider\",\n"
+ + " \"subject_token_type\": \"urn:ietf:params:oauth:token-type:jwt\",\n"
+ + " \"actor_token_type\": \"urn:ietf:params:oauth:token-type:jwt\",\n"
+ + " \"token_url\": \""
+ + transportFactory.transport.getStsUrl()
+ + "\",\n"
+ + " \"credential_source\": {\n"
+ + " \"file\": \""
+ + tokenFile.toString().replace("\\", "\\\\")
+ + "\",\n"
+ + " \"format\": {\n"
+ + " \"type\": \"json\",\n"
+ + " \"subject_token_field_name\": \"subject_token\",\n"
+ + " \"actor_token_field_name\": \"actor_token\"\n"
+ + " }\n"
+ + " }\n"
+ + "}";
+
+ ExternalAccountCredentials credentials =
+ ExternalAccountCredentials.fromStream(
+ new ByteArrayInputStream(configJson.getBytes(StandardCharsets.UTF_8)),
+ transportFactory);
+
+ assertTrue(credentials instanceof IdentityPoolCredentials);
+ IdentityPoolCredentials idp = (IdentityPoolCredentials) credentials;
+ assertNull(idp.getX509Provider());
+ assertEquals("urn:ietf:params:oauth:token-type:jwt", idp.getActorTokenType());
+ assertFalse(idp.getTransportFactory() instanceof MtlsHttpTransportFactory);
+ assertSame(idp.getIdentityPoolSubjectTokenSupplier(), idp.getIdentityPoolActorTokenSupplier());
+
+ AccessToken token = idp.refreshAccessToken();
+ assertNotNull(token);
+ Map