From 4d4f171a4641e907b9230e545700aec12c05d6b3 Mon Sep 17 00:00:00 2001 From: Leo Siracusa Date: Sat, 19 Sep 2026 14:44:52 +0000 Subject: [PATCH 1/8] fix(auth): avoid redundant executable run and preserve impersonated email in PluggableAuthCredentials --- .../oauth2/ExternalAccountCredentials.java | 21 +++++--- .../auth/oauth2/PluggableAuthCredentials.java | 30 +++++++++++- .../oauth2/PluggableAuthCredentialsTest.java | 49 ++++++++++++++++++- 3 files changed, 89 insertions(+), 11 deletions(-) diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index 0489d1ad743d..6378f15fb597 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -286,6 +286,9 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder) if (serviceAccountImpersonationUrl == null) { return null; } + String targetPrincipal = + ImpersonatedCredentials.extractTargetPrincipal(serviceAccountImpersonationUrl); + // Create a copy of this instance without service account impersonation. ExternalAccountCredentials sourceCredentials; if (this instanceof AwsCredentials) { @@ -297,6 +300,7 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder) sourceCredentials = PluggableAuthCredentials.newBuilder((PluggableAuthCredentials) this) .setServiceAccountImpersonationUrl(null) + .setImpersonatedServiceAccountEmail(targetPrincipal) .build(); } else { sourceCredentials = @@ -305,8 +309,6 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder) .build(); } - String targetPrincipal = - ImpersonatedCredentials.extractTargetPrincipal(serviceAccountImpersonationUrl); return ImpersonatedCredentials.newBuilder() .setSourceCredentials(sourceCredentials) .setHttpTransportFactory(transportFactory) @@ -524,6 +526,13 @@ private boolean shouldBuildImpersonatedCredential() { return this.serviceAccountImpersonationUrl != null && this.impersonatedCredentials == null; } + @Nullable ImpersonatedCredentials getImpersonatedCredentials() { + if (this.shouldBuildImpersonatedCredential()) { + this.impersonatedCredentials = this.buildImpersonatedCredentials(); + } + return this.impersonatedCredentials; + } + /** * Exchanges the external credential for a Google Cloud access token. * @@ -534,11 +543,9 @@ private boolean shouldBuildImpersonatedCredential() { protected AccessToken exchangeExternalCredentialForAccessToken( StsTokenExchangeRequest stsTokenExchangeRequest) throws IOException { // Handle service account impersonation if necessary. - if (this.shouldBuildImpersonatedCredential()) { - this.impersonatedCredentials = this.buildImpersonatedCredentials(); - } - if (this.impersonatedCredentials != null) { - return this.impersonatedCredentials.refreshAccessToken(); + ImpersonatedCredentials impersonated = getImpersonatedCredentials(); + if (impersonated != null) { + return impersonated.refreshAccessToken(); } StsRequestHandler.Builder requestHandler = diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java index 10ab650c77e5..59c1c0b17d4a 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java @@ -107,10 +107,13 @@ public class PluggableAuthCredentials extends ExternalAccountCredentials { private final ExecutableHandler handler; + private final @Nullable String impersonatedServiceAccountEmail; + /** Internal constructor. See {@link Builder}. */ PluggableAuthCredentials(Builder builder) { super(builder); this.config = (PluggableAuthCredentialSource) builder.credentialSource; + this.impersonatedServiceAccountEmail = builder.impersonatedServiceAccountEmail; if (builder.handler != null) { handler = builder.handler; @@ -121,6 +124,10 @@ public class PluggableAuthCredentials extends ExternalAccountCredentials { @Override public AccessToken refreshAccessToken() throws IOException { + ImpersonatedCredentials impersonated = getImpersonatedCredentials(); + if (impersonated != null) { + return impersonated.refreshAccessToken(); + } String credential = retrieveSubjectToken(); StsTokenExchangeRequest.Builder stsTokenExchangeRequest = StsTokenExchangeRequest.newBuilder(credential, getSubjectTokenType()) @@ -150,8 +157,9 @@ public String retrieveSubjectToken() throws IOException { envMap.put("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE", getSubjectTokenType()); // Always set to 0 for Workload Identity Federation. envMap.put("GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE", "0"); - if (getServiceAccountEmail() != null) { - envMap.put("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL", getServiceAccountEmail()); + String serviceAccountEmail = getServiceAccountEmail(); + if (serviceAccountEmail != null) { + envMap.put("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL", serviceAccountEmail); } if (outputFilePath != null && !outputFilePath.isEmpty()) { envMap.put("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE", outputFilePath); @@ -185,6 +193,15 @@ public String getOutputFilePath() { return this.handler.retrieveTokenFromExecutable(options); } + @Override + public @Nullable String getServiceAccountEmail() { + String email = super.getServiceAccountEmail(); + if (email != null) { + return email; + } + return impersonatedServiceAccountEmail; + } + /** Clones the PluggableAuthCredentials with the specified scopes. */ @Override public PluggableAuthCredentials createScoped(Collection newScopes) { @@ -217,12 +234,14 @@ ExecutableHandler getExecutableHandler() { public static class Builder extends ExternalAccountCredentials.Builder { private @Nullable ExecutableHandler handler; + private @Nullable String impersonatedServiceAccountEmail; Builder() {} Builder(PluggableAuthCredentials credentials) { super(credentials); this.handler = credentials.handler; + this.impersonatedServiceAccountEmail = credentials.impersonatedServiceAccountEmail; } @CanIgnoreReturnValue @@ -277,6 +296,13 @@ public Builder setCredentialSource(PluggableAuthCredentialSource credentialSourc public Builder setServiceAccountImpersonationUrl( @Nullable String serviceAccountImpersonationUrl) { super.setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl); + this.impersonatedServiceAccountEmail = null; + return this; + } + + @CanIgnoreReturnValue + Builder setImpersonatedServiceAccountEmail(@Nullable String impersonatedServiceAccountEmail) { + this.impersonatedServiceAccountEmail = impersonatedServiceAccountEmail; return this; } diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java index a07d9450de35..a38df8c5b5f6 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java @@ -212,6 +212,15 @@ void refreshAccessToken_withServiceAccountImpersonation() throws IOException { transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); + final int[] invocationCount = {0}; + final ExecutableOptions[] providedOptions = {null}; + ExecutableHandler executableHandler = + options -> { + invocationCount[0]++; + providedOptions[0] = options; + return "pluggableAuthToken"; + }; + PluggableAuthCredentials credential = PluggableAuthCredentials.newBuilder() .setAudience( @@ -227,11 +236,15 @@ void refreshAccessToken_withServiceAccountImpersonation() throws IOException { credential = PluggableAuthCredentials.newBuilder(credential) - .setExecutableHandler(options -> "pluggableAuthToken") + .setExecutableHandler(executableHandler) .build(); AccessToken accessToken = credential.refreshAccessToken(); + assertEquals(1, invocationCount[0]); + assertEquals( + credential.getServiceAccountEmail(), + providedOptions[0].getEnvironmentMap().get("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL")); assertEquals( transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue()); @@ -253,6 +266,15 @@ void refreshAccessToken_withServiceAccountImpersonationOptions() throws IOExcept transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); + final int[] invocationCount = {0}; + final ExecutableOptions[] providedOptions = {null}; + ExecutableHandler executableHandler = + options -> { + invocationCount[0]++; + providedOptions[0] = options; + return "pluggableAuthToken"; + }; + PluggableAuthCredentials credential = PluggableAuthCredentials.newBuilder() .setAudience( @@ -270,11 +292,15 @@ void refreshAccessToken_withServiceAccountImpersonationOptions() throws IOExcept credential = PluggableAuthCredentials.newBuilder(credential) - .setExecutableHandler(options -> "pluggableAuthToken") + .setExecutableHandler(executableHandler) .build(); AccessToken accessToken = credential.refreshAccessToken(); + assertEquals(1, invocationCount[0]); + assertEquals( + credential.getServiceAccountEmail(), + providedOptions[0].getEnvironmentMap().get("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL")); assertEquals( transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue()); @@ -585,6 +611,25 @@ void createdScoped_clonedCredentialWithAddedScopes() { assertEquals("universeDomain", newCredentials.getUniverseDomain()); } + @Test + void createScoped_preservesImpersonatedServiceAccountEmail() { + PluggableAuthCredentials sourceCredentials = + PluggableAuthCredentials.newBuilder(CREDENTIAL) + .setServiceAccountImpersonationUrl(null) + .setImpersonatedServiceAccountEmail("testn@test.iam.gserviceaccount.com") + .build(); + + PluggableAuthCredentials scopedCredentials = + sourceCredentials.createScoped(Arrays.asList("scope1")); + + assertNull(scopedCredentials.getServiceAccountImpersonationUrl()); + assertEquals("testn@test.iam.gserviceaccount.com", scopedCredentials.getServiceAccountEmail()); + + PluggableAuthCredentials clearedCredentials = + scopedCredentials.toBuilder().setServiceAccountImpersonationUrl(null).build(); + assertNull(clearedCredentials.getServiceAccountEmail()); + } + @Test void serialize() { PluggableAuthCredentials testCredentials = From 93408f366b7e994f235d4ce5f2e3f52131a05c54 Mon Sep 17 00:00:00 2001 From: Leo Siracusa Date: Sat, 19 Sep 2026 18:33:01 +0000 Subject: [PATCH 2/8] fix(auth): address adversarial review findings in PluggableAuthCredentials --- .../oauth2/ExternalAccountCredentials.java | 26 ++++-- .../auth/oauth2/PluggableAuthCredentials.java | 12 +++ .../oauth2/PluggableAuthCredentialsTest.java | 85 +++++++++++++++---- 3 files changed, 98 insertions(+), 25 deletions(-) diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index 6378f15fb597..4c875fa91080 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -95,7 +95,7 @@ public abstract class ExternalAccountCredentials extends GoogleCredentials { protected transient HttpTransportFactory transportFactory; - protected @Nullable ImpersonatedCredentials impersonatedCredentials; + protected volatile @Nullable ImpersonatedCredentials impersonatedCredentials; private final EnvironmentProvider environmentProvider; private final PropertyProvider propertyProvider; @@ -290,17 +290,20 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder) ImpersonatedCredentials.extractTargetPrincipal(serviceAccountImpersonationUrl); // Create a copy of this instance without service account impersonation. - ExternalAccountCredentials sourceCredentials; + GoogleCredentials sourceCredentials; if (this instanceof AwsCredentials) { sourceCredentials = AwsCredentials.newBuilder((AwsCredentials) this) .setServiceAccountImpersonationUrl(null) .build(); } else if (this instanceof PluggableAuthCredentials) { + // Clear serviceAccountImpersonationUrl to prevent infinite recursion while preserving + // targetPrincipal for GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL. sourceCredentials = PluggableAuthCredentials.newBuilder((PluggableAuthCredentials) this) .setServiceAccountImpersonationUrl(null) .setImpersonatedServiceAccountEmail(targetPrincipal) + .setAccessToken(null) .build(); } else { sourceCredentials = @@ -522,15 +525,20 @@ private static boolean isAwsCredential(Map credentialSource) { && ((String) credentialSource.get("environment_id")).startsWith("aws"); } - private boolean shouldBuildImpersonatedCredential() { - return this.serviceAccountImpersonationUrl != null && this.impersonatedCredentials == null; - } - @Nullable ImpersonatedCredentials getImpersonatedCredentials() { - if (this.shouldBuildImpersonatedCredential()) { - this.impersonatedCredentials = this.buildImpersonatedCredentials(); + if (this.serviceAccountImpersonationUrl == null) { + return null; + } + ImpersonatedCredentials localRef = this.impersonatedCredentials; + if (localRef == null) { + synchronized (this.lock) { + localRef = this.impersonatedCredentials; + if (localRef == null) { + this.impersonatedCredentials = localRef = this.buildImpersonatedCredentials(); + } + } } - return this.impersonatedCredentials; + return localRef; } /** diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java index 59c1c0b17d4a..e67a8fb82abc 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java @@ -124,10 +124,12 @@ public class PluggableAuthCredentials extends ExternalAccountCredentials { @Override public AccessToken refreshAccessToken() throws IOException { + // Handle service account impersonation if necessary. ImpersonatedCredentials impersonated = getImpersonatedCredentials(); if (impersonated != null) { return impersonated.refreshAccessToken(); } + String credential = retrieveSubjectToken(); StsTokenExchangeRequest.Builder stsTokenExchangeRequest = StsTokenExchangeRequest.newBuilder(credential, getSubjectTokenType()) @@ -199,6 +201,8 @@ public String getOutputFilePath() { if (email != null) { return email; } + // Fall back to impersonatedServiceAccountEmail when serviceAccountImpersonationUrl is cleared + // on the inner sourceCredentials copy. return impersonatedServiceAccountEmail; } @@ -300,9 +304,17 @@ public Builder setServiceAccountImpersonationUrl( return this; } + /** + * Preserves the impersonated service account email for {@code + * GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL} on the inner source credentials after clearing + * {@code serviceAccountImpersonationUrl}. + */ @CanIgnoreReturnValue Builder setImpersonatedServiceAccountEmail(@Nullable String impersonatedServiceAccountEmail) { this.impersonatedServiceAccountEmail = impersonatedServiceAccountEmail; + if (impersonatedServiceAccountEmail != null) { + super.setServiceAccountImpersonationUrl(null); + } return this; } diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java index a38df8c5b5f6..62647e2cbb4b 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java @@ -35,6 +35,7 @@ import static com.google.auth.oauth2.MockExternalAccountCredentialsTransport.SERVICE_ACCOUNT_IMPERSONATION_URL; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import com.google.api.client.http.HttpTransport; @@ -48,6 +49,7 @@ import java.io.NotSerializableException; import java.math.BigDecimal; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -63,6 +65,7 @@ class PluggableAuthCredentialsTest extends BaseSerializationTest { // The maximum timeout for waiting for the executable to finish (120 seconds). private static final int MAXIMUM_EXECUTABLE_TIMEOUT_MS = 120 * 1000; private static final String STS_URL = "https://sts.googleapis.com"; + private static final String IMPERSONATED_EMAIL = "testn@test.iam.gserviceaccount.com"; private static final PluggableAuthCredentials CREDENTIAL = PluggableAuthCredentials.newBuilder() @@ -229,21 +232,18 @@ void refreshAccessToken_withServiceAccountImpersonation() throws IOException { .setTokenInfoUrl("tokenInfoUrl") .setTokenUrl(transportFactory.transport.getStsUrl()) .setCredentialSource(buildCredentialSource()) + .setExecutableHandler(executableHandler) .setServiceAccountImpersonationUrl( transportFactory.transport.getServiceAccountImpersonationUrl()) .setHttpTransportFactory(transportFactory) .build(); - credential = - PluggableAuthCredentials.newBuilder(credential) - .setExecutableHandler(executableHandler) - .build(); - AccessToken accessToken = credential.refreshAccessToken(); + // Validate that the executable was invoked once with the impersonated email. assertEquals(1, invocationCount[0]); assertEquals( - credential.getServiceAccountEmail(), + IMPERSONATED_EMAIL, providedOptions[0].getEnvironmentMap().get("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL")); assertEquals( transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue()); @@ -257,6 +257,11 @@ void refreshAccessToken_withServiceAccountImpersonation() throws IOException { Map> headers = transportFactory.transport.getRequests().get(0).getHeaders(); ExternalAccountCredentialsTest.validateMetricsHeader(headers, "executable", true, false); + + // Validate that refreshing a second time reuses cached impersonatedCredentials and does not + // re-invoke the executable while the source STS token is still unexpired. + credential.refreshAccessToken(); + assertEquals(1, invocationCount[0]); } @Test @@ -283,6 +288,7 @@ void refreshAccessToken_withServiceAccountImpersonationOptions() throws IOExcept .setTokenInfoUrl("tokenInfoUrl") .setTokenUrl(transportFactory.transport.getStsUrl()) .setCredentialSource(buildCredentialSource()) + .setExecutableHandler(executableHandler) .setServiceAccountImpersonationUrl( transportFactory.transport.getServiceAccountImpersonationUrl()) .setServiceAccountImpersonationOptions( @@ -290,16 +296,12 @@ void refreshAccessToken_withServiceAccountImpersonationOptions() throws IOExcept .setHttpTransportFactory(transportFactory) .build(); - credential = - PluggableAuthCredentials.newBuilder(credential) - .setExecutableHandler(executableHandler) - .build(); - AccessToken accessToken = credential.refreshAccessToken(); + // Validate that the executable was invoked once with the impersonated email. assertEquals(1, invocationCount[0]); assertEquals( - credential.getServiceAccountEmail(), + IMPERSONATED_EMAIL, providedOptions[0].getEnvironmentMap().get("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL")); assertEquals( transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue()); @@ -318,6 +320,36 @@ void refreshAccessToken_withServiceAccountImpersonationOptions() throws IOExcept ExternalAccountCredentialsTest.validateMetricsHeader(headers, "executable", true, true); } + @Test + void refreshAccessToken_withServiceAccountImpersonation_executableFailure() { + MockExternalAccountCredentialsTransportFactory transportFactory = + new MockExternalAccountCredentialsTransportFactory(); + + PluggableAuthException expectedException = + new PluggableAuthException("INVALID_EXECUTABLE", "Executable failed."); + ExecutableHandler executableHandler = + options -> { + throw expectedException; + }; + + PluggableAuthCredentials credential = + PluggableAuthCredentials.newBuilder() + .setAudience( + "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider") + .setSubjectTokenType("subjectTokenType") + .setTokenInfoUrl("tokenInfoUrl") + .setTokenUrl(transportFactory.transport.getStsUrl()) + .setCredentialSource(buildCredentialSource()) + .setExecutableHandler(executableHandler) + .setServiceAccountImpersonationUrl( + transportFactory.transport.getServiceAccountImpersonationUrl()) + .setHttpTransportFactory(transportFactory) + .build(); + + IOException exception = assertThrows(IOException.class, credential::refreshAccessToken); + assertSame(expectedException, exception.getCause()); + } + @Test void pluggableAuthCredentialSource_allFields() { Map source = new HashMap<>(); @@ -613,17 +645,38 @@ void createdScoped_clonedCredentialWithAddedScopes() { @Test void createScoped_preservesImpersonatedServiceAccountEmail() { + PluggableAuthCredentials outerCredentials = + (PluggableAuthCredentials) + PluggableAuthCredentials.newBuilder(CREDENTIAL) + .setServiceAccountImpersonationUrl( + "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/" + + IMPERSONATED_EMAIL + + ":generateAccessToken") + .setAccessToken(new AccessToken("cached-outer-token", null)) + .build(); + assertNull( + outerCredentials.buildImpersonatedCredentials().getSourceCredentials().getAccessToken()); + PluggableAuthCredentials sourceCredentials = - PluggableAuthCredentials.newBuilder(CREDENTIAL) + PluggableAuthCredentials.newBuilder(outerCredentials) .setServiceAccountImpersonationUrl(null) - .setImpersonatedServiceAccountEmail("testn@test.iam.gserviceaccount.com") + .setImpersonatedServiceAccountEmail(IMPERSONATED_EMAIL) .build(); PluggableAuthCredentials scopedCredentials = - sourceCredentials.createScoped(Arrays.asList("scope1")); + sourceCredentials.createScoped(Collections.singletonList("scope1")); assertNull(scopedCredentials.getServiceAccountImpersonationUrl()); - assertEquals("testn@test.iam.gserviceaccount.com", scopedCredentials.getServiceAccountEmail()); + assertEquals(IMPERSONATED_EMAIL, scopedCredentials.getServiceAccountEmail()); + + // Verify that setting impersonatedServiceAccountEmail also clears + // serviceAccountImpersonationUrl regardless of setter call order. + PluggableAuthCredentials reorderedCredentials = + PluggableAuthCredentials.newBuilder(outerCredentials) + .setImpersonatedServiceAccountEmail(IMPERSONATED_EMAIL) + .build(); + assertNull(reorderedCredentials.getServiceAccountImpersonationUrl()); + assertEquals(IMPERSONATED_EMAIL, reorderedCredentials.getServiceAccountEmail()); PluggableAuthCredentials clearedCredentials = scopedCredentials.toBuilder().setServiceAccountImpersonationUrl(null).build(); From 69694000d012dbe3e634bbe74c94f1bf105766d3 Mon Sep 17 00:00:00 2001 From: Leo Siracusa Date: Sat, 19 Sep 2026 20:01:15 +0000 Subject: [PATCH 3/8] refactor(auth): simplify getImpersonatedCredentials and impersonated email handling --- .../oauth2/ExternalAccountCredentials.java | 35 ++++++++----------- .../auth/oauth2/PluggableAuthCredentials.java | 28 +++------------ .../oauth2/PluggableAuthCredentialsTest.java | 27 +------------- 3 files changed, 20 insertions(+), 70 deletions(-) diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index 4c875fa91080..c98e4038721a 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -286,24 +286,19 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder) if (serviceAccountImpersonationUrl == null) { return null; } - String targetPrincipal = - ImpersonatedCredentials.extractTargetPrincipal(serviceAccountImpersonationUrl); // Create a copy of this instance without service account impersonation. - GoogleCredentials sourceCredentials; + ExternalAccountCredentials sourceCredentials; if (this instanceof AwsCredentials) { sourceCredentials = AwsCredentials.newBuilder((AwsCredentials) this) .setServiceAccountImpersonationUrl(null) .build(); } else if (this instanceof PluggableAuthCredentials) { - // Clear serviceAccountImpersonationUrl to prevent infinite recursion while preserving - // targetPrincipal for GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL. sourceCredentials = PluggableAuthCredentials.newBuilder((PluggableAuthCredentials) this) .setServiceAccountImpersonationUrl(null) - .setImpersonatedServiceAccountEmail(targetPrincipal) - .setAccessToken(null) + .setImpersonatedServiceAccountEmail(getServiceAccountEmail()) .build(); } else { sourceCredentials = @@ -312,6 +307,8 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder) .build(); } + String targetPrincipal = + ImpersonatedCredentials.extractTargetPrincipal(serviceAccountImpersonationUrl); return ImpersonatedCredentials.newBuilder() .setSourceCredentials(sourceCredentials) .setHttpTransportFactory(transportFactory) @@ -525,20 +522,17 @@ private static boolean isAwsCredential(Map credentialSource) { && ((String) credentialSource.get("environment_id")).startsWith("aws"); } + private boolean shouldBuildImpersonatedCredential() { + return this.serviceAccountImpersonationUrl != null && this.impersonatedCredentials == null; + } + @Nullable ImpersonatedCredentials getImpersonatedCredentials() { - if (this.serviceAccountImpersonationUrl == null) { - return null; - } - ImpersonatedCredentials localRef = this.impersonatedCredentials; - if (localRef == null) { - synchronized (this.lock) { - localRef = this.impersonatedCredentials; - if (localRef == null) { - this.impersonatedCredentials = localRef = this.buildImpersonatedCredentials(); - } + synchronized (this.lock) { + if (this.shouldBuildImpersonatedCredential()) { + this.impersonatedCredentials = this.buildImpersonatedCredentials(); } + return this.impersonatedCredentials; } - return localRef; } /** @@ -551,9 +545,8 @@ private static boolean isAwsCredential(Map credentialSource) { protected AccessToken exchangeExternalCredentialForAccessToken( StsTokenExchangeRequest stsTokenExchangeRequest) throws IOException { // Handle service account impersonation if necessary. - ImpersonatedCredentials impersonated = getImpersonatedCredentials(); - if (impersonated != null) { - return impersonated.refreshAccessToken(); + if (getImpersonatedCredentials() != null) { + return this.impersonatedCredentials.refreshAccessToken(); } StsRequestHandler.Builder requestHandler = diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java index e67a8fb82abc..9996f4a15c85 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java @@ -124,12 +124,9 @@ public class PluggableAuthCredentials extends ExternalAccountCredentials { @Override public AccessToken refreshAccessToken() throws IOException { - // Handle service account impersonation if necessary. - ImpersonatedCredentials impersonated = getImpersonatedCredentials(); - if (impersonated != null) { - return impersonated.refreshAccessToken(); + if (getImpersonatedCredentials() != null) { + return this.impersonatedCredentials.refreshAccessToken(); } - String credential = retrieveSubjectToken(); StsTokenExchangeRequest.Builder stsTokenExchangeRequest = StsTokenExchangeRequest.newBuilder(credential, getSubjectTokenType()) @@ -159,9 +156,8 @@ public String retrieveSubjectToken() throws IOException { envMap.put("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE", getSubjectTokenType()); // Always set to 0 for Workload Identity Federation. envMap.put("GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE", "0"); - String serviceAccountEmail = getServiceAccountEmail(); - if (serviceAccountEmail != null) { - envMap.put("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL", serviceAccountEmail); + if (getServiceAccountEmail() != null) { + envMap.put("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL", getServiceAccountEmail()); } if (outputFilePath != null && !outputFilePath.isEmpty()) { envMap.put("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE", outputFilePath); @@ -198,12 +194,7 @@ public String getOutputFilePath() { @Override public @Nullable String getServiceAccountEmail() { String email = super.getServiceAccountEmail(); - if (email != null) { - return email; - } - // Fall back to impersonatedServiceAccountEmail when serviceAccountImpersonationUrl is cleared - // on the inner sourceCredentials copy. - return impersonatedServiceAccountEmail; + return email != null ? email : impersonatedServiceAccountEmail; } /** Clones the PluggableAuthCredentials with the specified scopes. */ @@ -300,21 +291,12 @@ public Builder setCredentialSource(PluggableAuthCredentialSource credentialSourc public Builder setServiceAccountImpersonationUrl( @Nullable String serviceAccountImpersonationUrl) { super.setServiceAccountImpersonationUrl(serviceAccountImpersonationUrl); - this.impersonatedServiceAccountEmail = null; return this; } - /** - * Preserves the impersonated service account email for {@code - * GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL} on the inner source credentials after clearing - * {@code serviceAccountImpersonationUrl}. - */ @CanIgnoreReturnValue Builder setImpersonatedServiceAccountEmail(@Nullable String impersonatedServiceAccountEmail) { this.impersonatedServiceAccountEmail = impersonatedServiceAccountEmail; - if (impersonatedServiceAccountEmail != null) { - super.setServiceAccountImpersonationUrl(null); - } return this; } diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java index 62647e2cbb4b..6de94a876a1b 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java @@ -645,20 +645,8 @@ void createdScoped_clonedCredentialWithAddedScopes() { @Test void createScoped_preservesImpersonatedServiceAccountEmail() { - PluggableAuthCredentials outerCredentials = - (PluggableAuthCredentials) - PluggableAuthCredentials.newBuilder(CREDENTIAL) - .setServiceAccountImpersonationUrl( - "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/" - + IMPERSONATED_EMAIL - + ":generateAccessToken") - .setAccessToken(new AccessToken("cached-outer-token", null)) - .build(); - assertNull( - outerCredentials.buildImpersonatedCredentials().getSourceCredentials().getAccessToken()); - PluggableAuthCredentials sourceCredentials = - PluggableAuthCredentials.newBuilder(outerCredentials) + PluggableAuthCredentials.newBuilder(CREDENTIAL) .setServiceAccountImpersonationUrl(null) .setImpersonatedServiceAccountEmail(IMPERSONATED_EMAIL) .build(); @@ -668,19 +656,6 @@ void createScoped_preservesImpersonatedServiceAccountEmail() { assertNull(scopedCredentials.getServiceAccountImpersonationUrl()); assertEquals(IMPERSONATED_EMAIL, scopedCredentials.getServiceAccountEmail()); - - // Verify that setting impersonatedServiceAccountEmail also clears - // serviceAccountImpersonationUrl regardless of setter call order. - PluggableAuthCredentials reorderedCredentials = - PluggableAuthCredentials.newBuilder(outerCredentials) - .setImpersonatedServiceAccountEmail(IMPERSONATED_EMAIL) - .build(); - assertNull(reorderedCredentials.getServiceAccountImpersonationUrl()); - assertEquals(IMPERSONATED_EMAIL, reorderedCredentials.getServiceAccountEmail()); - - PluggableAuthCredentials clearedCredentials = - scopedCredentials.toBuilder().setServiceAccountImpersonationUrl(null).build(); - assertNull(clearedCredentials.getServiceAccountEmail()); } @Test From 4c6c1b9a9b059323a85aaa474ae3fae0ff3e1449 Mon Sep 17 00:00:00 2001 From: Leo Siracusa Date: Sat, 19 Sep 2026 21:16:17 +0000 Subject: [PATCH 4/8] refactor(auth): capture impersonatedServiceAccountEmail in PluggableAuthCredentials.Builder constructor --- .../auth/oauth2/ExternalAccountCredentials.java | 1 - .../google/auth/oauth2/PluggableAuthCredentials.java | 8 +------- .../auth/oauth2/PluggableAuthCredentialsTest.java | 11 ++++++++--- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index c98e4038721a..6b9262913bc4 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -298,7 +298,6 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder) sourceCredentials = PluggableAuthCredentials.newBuilder((PluggableAuthCredentials) this) .setServiceAccountImpersonationUrl(null) - .setImpersonatedServiceAccountEmail(getServiceAccountEmail()) .build(); } else { sourceCredentials = diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java index 9996f4a15c85..23866b5d029c 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java @@ -236,7 +236,7 @@ public static class Builder extends ExternalAccountCredentials.Builder { Builder(PluggableAuthCredentials credentials) { super(credentials); this.handler = credentials.handler; - this.impersonatedServiceAccountEmail = credentials.impersonatedServiceAccountEmail; + this.impersonatedServiceAccountEmail = credentials.getServiceAccountEmail(); } @CanIgnoreReturnValue @@ -294,12 +294,6 @@ public Builder setServiceAccountImpersonationUrl( return this; } - @CanIgnoreReturnValue - Builder setImpersonatedServiceAccountEmail(@Nullable String impersonatedServiceAccountEmail) { - this.impersonatedServiceAccountEmail = impersonatedServiceAccountEmail; - return this; - } - @Override @CanIgnoreReturnValue public Builder setTokenInfoUrl(String tokenInfoUrl) { diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java index 6de94a876a1b..3e25fc23ecb3 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java @@ -645,11 +645,16 @@ void createdScoped_clonedCredentialWithAddedScopes() { @Test void createScoped_preservesImpersonatedServiceAccountEmail() { - PluggableAuthCredentials sourceCredentials = + PluggableAuthCredentials outerCredentials = PluggableAuthCredentials.newBuilder(CREDENTIAL) - .setServiceAccountImpersonationUrl(null) - .setImpersonatedServiceAccountEmail(IMPERSONATED_EMAIL) + .setServiceAccountImpersonationUrl( + "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/" + + IMPERSONATED_EMAIL + + ":generateAccessToken") .build(); + PluggableAuthCredentials sourceCredentials = + (PluggableAuthCredentials) + outerCredentials.buildImpersonatedCredentials().getSourceCredentials(); PluggableAuthCredentials scopedCredentials = sourceCredentials.createScoped(Collections.singletonList("scope1")); From c120b587b3a13397dbb7fbfd9efc43861124af48 Mon Sep 17 00:00:00 2001 From: Leo Siracusa Date: Sat, 19 Sep 2026 22:00:24 +0000 Subject: [PATCH 5/8] refactor(auth): remove volatile and keep impersonatedServiceAccountEmail private --- .../oauth2/ExternalAccountCredentials.java | 2 +- .../auth/oauth2/PluggableAuthCredentials.java | 19 ++++++++++--------- .../oauth2/PluggableAuthCredentialsTest.java | 13 +++++++++++-- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index 6b9262913bc4..d5a50864b8a9 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -95,7 +95,7 @@ public abstract class ExternalAccountCredentials extends GoogleCredentials { protected transient HttpTransportFactory transportFactory; - protected volatile @Nullable ImpersonatedCredentials impersonatedCredentials; + protected @Nullable ImpersonatedCredentials impersonatedCredentials; private final EnvironmentProvider environmentProvider; private final PropertyProvider propertyProvider; diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java index 23866b5d029c..4f5990944e4a 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java @@ -156,8 +156,12 @@ public String retrieveSubjectToken() throws IOException { envMap.put("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE", getSubjectTokenType()); // Always set to 0 for Workload Identity Federation. envMap.put("GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE", "0"); - if (getServiceAccountEmail() != null) { - envMap.put("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL", getServiceAccountEmail()); + String serviceAccountEmail = + getServiceAccountEmail() != null + ? getServiceAccountEmail() + : impersonatedServiceAccountEmail; + if (serviceAccountEmail != null) { + envMap.put("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL", serviceAccountEmail); } if (outputFilePath != null && !outputFilePath.isEmpty()) { envMap.put("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE", outputFilePath); @@ -191,12 +195,6 @@ public String getOutputFilePath() { return this.handler.retrieveTokenFromExecutable(options); } - @Override - public @Nullable String getServiceAccountEmail() { - String email = super.getServiceAccountEmail(); - return email != null ? email : impersonatedServiceAccountEmail; - } - /** Clones the PluggableAuthCredentials with the specified scopes. */ @Override public PluggableAuthCredentials createScoped(Collection newScopes) { @@ -236,7 +234,10 @@ public static class Builder extends ExternalAccountCredentials.Builder { Builder(PluggableAuthCredentials credentials) { super(credentials); this.handler = credentials.handler; - this.impersonatedServiceAccountEmail = credentials.getServiceAccountEmail(); + this.impersonatedServiceAccountEmail = + credentials.getServiceAccountEmail() != null + ? credentials.getServiceAccountEmail() + : credentials.impersonatedServiceAccountEmail; } @CanIgnoreReturnValue diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java index 3e25fc23ecb3..53f9cea7efd6 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/PluggableAuthCredentialsTest.java @@ -644,9 +644,16 @@ void createdScoped_clonedCredentialWithAddedScopes() { } @Test - void createScoped_preservesImpersonatedServiceAccountEmail() { + void createScoped_preservesImpersonatedServiceAccountEmail() throws IOException { + final String[] recordedImpersonatedEmail = {null}; PluggableAuthCredentials outerCredentials = PluggableAuthCredentials.newBuilder(CREDENTIAL) + .setExecutableHandler( + options -> { + recordedImpersonatedEmail[0] = + options.getEnvironmentMap().get("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL"); + return "pluggableAuthToken"; + }) .setServiceAccountImpersonationUrl( "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/" + IMPERSONATED_EMAIL @@ -658,9 +665,11 @@ void createScoped_preservesImpersonatedServiceAccountEmail() { PluggableAuthCredentials scopedCredentials = sourceCredentials.createScoped(Collections.singletonList("scope1")); + scopedCredentials.retrieveSubjectToken(); assertNull(scopedCredentials.getServiceAccountImpersonationUrl()); - assertEquals(IMPERSONATED_EMAIL, scopedCredentials.getServiceAccountEmail()); + assertNull(scopedCredentials.getServiceAccountEmail()); + assertEquals(IMPERSONATED_EMAIL, recordedImpersonatedEmail[0]); } @Test From 0d3b6230ee9e83c8443fc58948cf8bdc9d89090a Mon Sep 17 00:00:00 2001 From: Leo Siracusa Date: Sun, 20 Sep 2026 00:16:08 +0000 Subject: [PATCH 6/8] refactor(auth): revert ExternalAccountCredentials changes and call buildImpersonatedCredentials directly --- .../auth/oauth2/ExternalAccountCredentials.java | 15 ++++----------- .../auth/oauth2/PluggableAuthCredentials.java | 5 ++++- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java index d5a50864b8a9..0489d1ad743d 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ExternalAccountCredentials.java @@ -286,7 +286,6 @@ protected ExternalAccountCredentials(ExternalAccountCredentials.Builder builder) if (serviceAccountImpersonationUrl == null) { return null; } - // Create a copy of this instance without service account impersonation. ExternalAccountCredentials sourceCredentials; if (this instanceof AwsCredentials) { @@ -525,15 +524,6 @@ private boolean shouldBuildImpersonatedCredential() { return this.serviceAccountImpersonationUrl != null && this.impersonatedCredentials == null; } - @Nullable ImpersonatedCredentials getImpersonatedCredentials() { - synchronized (this.lock) { - if (this.shouldBuildImpersonatedCredential()) { - this.impersonatedCredentials = this.buildImpersonatedCredentials(); - } - return this.impersonatedCredentials; - } - } - /** * Exchanges the external credential for a Google Cloud access token. * @@ -544,7 +534,10 @@ private boolean shouldBuildImpersonatedCredential() { protected AccessToken exchangeExternalCredentialForAccessToken( StsTokenExchangeRequest stsTokenExchangeRequest) throws IOException { // Handle service account impersonation if necessary. - if (getImpersonatedCredentials() != null) { + if (this.shouldBuildImpersonatedCredential()) { + this.impersonatedCredentials = this.buildImpersonatedCredentials(); + } + if (this.impersonatedCredentials != null) { return this.impersonatedCredentials.refreshAccessToken(); } diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java index 4f5990944e4a..d844dc85223a 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java @@ -124,7 +124,10 @@ public class PluggableAuthCredentials extends ExternalAccountCredentials { @Override public AccessToken refreshAccessToken() throws IOException { - if (getImpersonatedCredentials() != null) { + if (this.impersonatedCredentials == null) { + this.impersonatedCredentials = this.buildImpersonatedCredentials(); + } + if (this.impersonatedCredentials != null) { return this.impersonatedCredentials.refreshAccessToken(); } String credential = retrieveSubjectToken(); From ad298c917c2ff49b4c4f6f694c783b5287128f9c Mon Sep 17 00:00:00 2001 From: Leo Siracusa Date: Sun, 20 Sep 2026 00:20:52 +0000 Subject: [PATCH 7/8] refactor(auth): guard impersonated refresh with getServiceAccountImpersonationUrl() != null --- .../com/google/auth/oauth2/PluggableAuthCredentials.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java index d844dc85223a..b7936ee42bde 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java @@ -124,10 +124,10 @@ public class PluggableAuthCredentials extends ExternalAccountCredentials { @Override public AccessToken refreshAccessToken() throws IOException { - if (this.impersonatedCredentials == null) { - this.impersonatedCredentials = this.buildImpersonatedCredentials(); - } - if (this.impersonatedCredentials != null) { + if (getServiceAccountImpersonationUrl() != null) { + if (this.impersonatedCredentials == null) { + this.impersonatedCredentials = this.buildImpersonatedCredentials(); + } return this.impersonatedCredentials.refreshAccessToken(); } String credential = retrieveSubjectToken(); From 857be0af602a63d17f72c4d9231525b9f7531671 Mon Sep 17 00:00:00 2001 From: Leo Siracusa Date: Sun, 20 Sep 2026 00:24:03 +0000 Subject: [PATCH 8/8] refactor(auth): resolve impersonatedServiceAccountEmail once in PluggableAuthCredentials constructor --- .../auth/oauth2/PluggableAuthCredentials.java | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java index b7936ee42bde..fcfdfdf29d7b 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/PluggableAuthCredentials.java @@ -113,7 +113,10 @@ public class PluggableAuthCredentials extends ExternalAccountCredentials { PluggableAuthCredentials(Builder builder) { super(builder); this.config = (PluggableAuthCredentialSource) builder.credentialSource; - this.impersonatedServiceAccountEmail = builder.impersonatedServiceAccountEmail; + this.impersonatedServiceAccountEmail = + getServiceAccountEmail() != null + ? getServiceAccountEmail() + : builder.impersonatedServiceAccountEmail; if (builder.handler != null) { handler = builder.handler; @@ -159,12 +162,8 @@ public String retrieveSubjectToken() throws IOException { envMap.put("GOOGLE_EXTERNAL_ACCOUNT_TOKEN_TYPE", getSubjectTokenType()); // Always set to 0 for Workload Identity Federation. envMap.put("GOOGLE_EXTERNAL_ACCOUNT_INTERACTIVE", "0"); - String serviceAccountEmail = - getServiceAccountEmail() != null - ? getServiceAccountEmail() - : impersonatedServiceAccountEmail; - if (serviceAccountEmail != null) { - envMap.put("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL", serviceAccountEmail); + if (impersonatedServiceAccountEmail != null) { + envMap.put("GOOGLE_EXTERNAL_ACCOUNT_IMPERSONATED_EMAIL", impersonatedServiceAccountEmail); } if (outputFilePath != null && !outputFilePath.isEmpty()) { envMap.put("GOOGLE_EXTERNAL_ACCOUNT_OUTPUT_FILE", outputFilePath); @@ -237,10 +236,7 @@ public static class Builder extends ExternalAccountCredentials.Builder { Builder(PluggableAuthCredentials credentials) { super(credentials); this.handler = credentials.handler; - this.impersonatedServiceAccountEmail = - credentials.getServiceAccountEmail() != null - ? credentials.getServiceAccountEmail() - : credentials.impersonatedServiceAccountEmail; + this.impersonatedServiceAccountEmail = credentials.impersonatedServiceAccountEmail; } @CanIgnoreReturnValue