From 8e5f0050df113ec17959eab76115311672db3be2 Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Fri, 18 Sep 2026 02:18:28 +0000 Subject: [PATCH 1/3] feat(oauth2): support non-mTLS token URLs and unbound actor tokens in IdentityPoolCredentials --- .../auth/oauth2/IdentityPoolCredentials.java | 64 +--- .../oauth2/IdentityPoolCredentialsTest.java | 293 +++++++++++------- 2 files changed, 189 insertions(+), 168 deletions(-) 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..1252162a3459 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,58 @@ 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); } @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); } @Test @@ -2697,52 +2682,43 @@ 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); } @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); } @Test @@ -3088,6 +3064,101 @@ 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, true); + + OAuthException thrown = assertThrows(OAuthException.class, testable::refreshAccessToken); + assertEquals(401, thrown.getHttpStatusCode()); + assertEquals(1, testable.getExchangeCallCount()); + } + + @Test + void + fromStream_fileCredentialSource_withoutCertificateConfig_andActorToken_withNonMtlsUrl_refreshesSuccessfully( + @TempDir Path tempDir) throws Exception { + 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\": \"https://sts.googleapis.com/v1/token\",\n" + + " \"credential_source\": {\n" + + " \"file\": \"" + + tokenFile.toString() + + "\",\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))); + + 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()); + + AtomicReference capturedRequest = new AtomicReference<>(); + AtomicReference capturedFactory = new AtomicReference<>(); + IdentityPoolCredentials testable = + new IdentityPoolCredentials(idp.toBuilder()) { + @Override + protected AccessToken exchangeExternalCredentialForAccessToken( + StsTokenExchangeRequest stsTokenExchangeRequest, + HttpTransportFactory cycleTransportFactory) { + capturedRequest.set(stsTokenExchangeRequest); + capturedFactory.set(cycleTransportFactory); + return new AccessToken("nonMtlsAccessToken", null); + } + }; + + AccessToken token = testable.refreshAccessToken(); + assertEquals("nonMtlsAccessToken", token.getTokenValue()); + assertNotNull(capturedRequest.get()); + assertEquals("nonMtlsSubjectToken", capturedRequest.get().getSubjectToken()); + assertNotNull(capturedRequest.get().getActingParty()); + assertEquals("nonMtlsActorToken", capturedRequest.get().getActingParty().getActorToken()); + assertEquals( + "urn:ietf:params:oauth:token-type:jwt", + capturedRequest.get().getActingParty().getActorTokenType()); + assertNotNull(capturedFactory.get()); + assertFalse(capturedFactory.get() instanceof MtlsHttpTransportFactory); + } + // ================================================================================== // Helper: TestableIdentityPoolCredentials — overrides exchange for 401 testing // ================================================================================== From 5b01057d42f7866aaec73b44086ffdda41fcc5b5 Mon Sep 17 00:00:00 2001 From: macastelaz <34776182+macastelaz@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:25:07 -0500 Subject: [PATCH 2/3] Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- .../com/google/auth/oauth2/IdentityPoolCredentialsTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1252162a3459..12e6234dbdaa 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 @@ -3111,7 +3111,7 @@ void refreshAccessToken_401WithActorTokenAndNonMtlsTransport_bubblesUpWithoutRet + " \"token_url\": \"https://sts.googleapis.com/v1/token\",\n" + " \"credential_source\": {\n" + " \"file\": \"" - + tokenFile.toString() + + tokenFile.toString().replace("\\", "\\\\") + "\",\n" + " \"format\": {\n" + " \"type\": \"json\",\n" From 82254dbfd95f151c82d593d322073369a9e302ce Mon Sep 17 00:00:00 2001 From: Matt Castelaz Date: Fri, 18 Sep 2026 03:12:07 +0000 Subject: [PATCH 3/3] test(oauth2): strengthen non-mTLS actor token unit tests --- .../oauth2/IdentityPoolCredentialsTest.java | 70 ++++++++++++------- 1 file changed, 43 insertions(+), 27 deletions(-) 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 12e6234dbdaa..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 @@ -1752,6 +1752,9 @@ void builder_actorTokenWithNoArgMtlsFactory_succeeds() throws Exception { .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 @@ -1772,6 +1775,9 @@ void builder_actorTokenWithEmptyMtlsFactory_succeeds() throws Exception { .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 @@ -2698,6 +2704,8 @@ void builder_actorTokenWithPlainPublicTokenUrl_succeeds() throws Exception { .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 @@ -2719,6 +2727,8 @@ void builder_actorTokenWithPlainPublicImpersonationUrl_succeeds() throws Excepti .setHttpTransportFactory(transportFactory) .build(); assertNotNull(cred); + assertEquals("urn:ietf:params:oauth:token-type:jwt", cred.getActorTokenType()); + assertEquals(MockExternalAccountCredentialsTransport.STS_MTLS_URL, cred.getTokenUrl()); } @Test @@ -3081,10 +3091,11 @@ void refreshAccessToken_401WithActorTokenAndNonMtlsTransport_bubblesUpWithoutRet .setHttpTransportFactory(transportFactory); TestableIdentityPoolCredentials testable = - new TestableIdentityPoolCredentials(builder, true, true); + 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()); } @@ -3092,6 +3103,9 @@ void refreshAccessToken_401WithActorTokenAndNonMtlsTransport_bubblesUpWithoutRet 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); @@ -3108,7 +3122,9 @@ void refreshAccessToken_401WithActorTokenAndNonMtlsTransport_bubblesUpWithoutRet + " \"//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\": \"https://sts.googleapis.com/v1/token\",\n" + + " \"token_url\": \"" + + transportFactory.transport.getStsUrl() + + "\",\n" + " \"credential_source\": {\n" + " \"file\": \"" + tokenFile.toString().replace("\\", "\\\\") @@ -3123,7 +3139,8 @@ void refreshAccessToken_401WithActorTokenAndNonMtlsTransport_bubblesUpWithoutRet ExternalAccountCredentials credentials = ExternalAccountCredentials.fromStream( - new ByteArrayInputStream(configJson.getBytes(StandardCharsets.UTF_8))); + new ByteArrayInputStream(configJson.getBytes(StandardCharsets.UTF_8)), + transportFactory); assertTrue(credentials instanceof IdentityPoolCredentials); IdentityPoolCredentials idp = (IdentityPoolCredentials) credentials; @@ -3132,31 +3149,30 @@ void refreshAccessToken_401WithActorTokenAndNonMtlsTransport_bubblesUpWithoutRet assertFalse(idp.getTransportFactory() instanceof MtlsHttpTransportFactory); assertSame(idp.getIdentityPoolSubjectTokenSupplier(), idp.getIdentityPoolActorTokenSupplier()); - AtomicReference capturedRequest = new AtomicReference<>(); - AtomicReference capturedFactory = new AtomicReference<>(); - IdentityPoolCredentials testable = - new IdentityPoolCredentials(idp.toBuilder()) { - @Override - protected AccessToken exchangeExternalCredentialForAccessToken( - StsTokenExchangeRequest stsTokenExchangeRequest, - HttpTransportFactory cycleTransportFactory) { - capturedRequest.set(stsTokenExchangeRequest); - capturedFactory.set(cycleTransportFactory); - return new AccessToken("nonMtlsAccessToken", null); - } - }; + AccessToken token = idp.refreshAccessToken(); + assertNotNull(token); + Map query = + TestUtils.parseQuery(transportFactory.transport.getLastRequest().getContentAsString()); + assertEquals("nonMtlsSubjectToken", query.get("subject_token")); + assertEquals("nonMtlsActorToken", query.get("actor_token")); + assertEquals("urn:ietf:params:oauth:token-type:jwt", query.get("actor_token_type")); - AccessToken token = testable.refreshAccessToken(); - assertEquals("nonMtlsAccessToken", token.getTokenValue()); - assertNotNull(capturedRequest.get()); - assertEquals("nonMtlsSubjectToken", capturedRequest.get().getSubjectToken()); - assertNotNull(capturedRequest.get().getActingParty()); - assertEquals("nonMtlsActorToken", capturedRequest.get().getActingParty().getActorToken()); - assertEquals( - "urn:ietf:params:oauth:token-type:jwt", - capturedRequest.get().getActingParty().getActorTokenType()); - assertNotNull(capturedFactory.get()); - assertFalse(capturedFactory.get() instanceof MtlsHttpTransportFactory); + // Verify createScoped preserves actor token config and atomic supplier sharing + IdentityPoolCredentials scoped = + idp.createScoped( + Collections.singletonList("https://www.googleapis.com/auth/cloud-platform")); + assertEquals("urn:ietf:params:oauth:token-type:jwt", scoped.getActorTokenType()); + assertNotNull(scoped.getIdentityPoolActorTokenSupplier()); + assertSame( + scoped.getIdentityPoolSubjectTokenSupplier(), scoped.getIdentityPoolActorTokenSupplier()); + + // Verify Java serialization/deserialization round-trip preserves actor token config + IdentityPoolCredentials deserialized = serializeAndDeserialize(idp); + assertEquals("urn:ietf:params:oauth:token-type:jwt", deserialized.getActorTokenType()); + assertNotNull(deserialized.getIdentityPoolActorTokenSupplier()); + assertSame( + deserialized.getIdentityPoolSubjectTokenSupplier(), + deserialized.getIdentityPoolActorTokenSupplier()); } // ==================================================================================