From e36234e43af4475dda8056b5f182f454013d1000 Mon Sep 17 00:00:00 2001 From: Leo Siracusa Date: Sun, 20 Sep 2026 00:43:02 +0000 Subject: [PATCH] fix(auth): avoid redundant retrieveSubjectToken call in AwsCredentials when service account impersonation is configured --- .../google/auth/oauth2/AwsCredentials.java | 6 ++++ .../auth/oauth2/AwsCredentialsTest.java | 33 +++++++++++++++++-- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java index 548008d4bab6..6cdbeae24725 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/AwsCredentials.java @@ -120,6 +120,12 @@ public class AwsCredentials extends ExternalAccountCredentials { @Override public AccessToken refreshAccessToken() throws IOException { + if (getServiceAccountImpersonationUrl() != null) { + if (this.impersonatedCredentials == null) { + this.impersonatedCredentials = this.buildImpersonatedCredentials(); + } + return this.impersonatedCredentials.refreshAccessToken(); + } StsTokenExchangeRequest.Builder stsTokenExchangeRequest = StsTokenExchangeRequest.newBuilder(retrieveSubjectToken(), getSubjectTokenType()) .setAudience(getAudience()); diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java index c7556c0ac3c6..1b3a2f9c50d5 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/AwsCredentialsTest.java @@ -165,10 +165,18 @@ void refreshAccessToken_withServiceAccountImpersonation() throws IOException { assertEquals( transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue()); + // 3 AWS metadata requests (region, role, credentials) + 1 STS + 1 IAM generateAccessToken. + assertEquals(5, transportFactory.transport.getRequests().size()); + // Validate metrics header is set correctly on the sts request. Map> headers = - transportFactory.transport.getRequests().get(6).getHeaders(); + transportFactory.transport.getRequests().get(3).getHeaders(); ExternalAccountCredentialsTest.validateMetricsHeader(headers, "aws", true, false); + + // Refreshing a second time reuses cached impersonatedCredentials and does not re-query AWS + // metadata while the source STS token is still unexpired. + awsCredential.refreshAccessToken(); + assertEquals(6, transportFactory.transport.getRequests().size()); } @Test @@ -196,6 +204,7 @@ void refreshAccessToken_withServiceAccountImpersonationOptions() throws IOExcept assertEquals( transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue()); + assertEquals(5, transportFactory.transport.getRequests().size()); // Validate that default lifetime was set correctly on the request. try (JsonParser jsonParser = @@ -206,7 +215,7 @@ void refreshAccessToken_withServiceAccountImpersonationOptions() throws IOExcept // Validate metrics header is set correctly on the sts request. Map> headers = - transportFactory.transport.getRequests().get(6).getHeaders(); + transportFactory.transport.getRequests().get(3).getHeaders(); ExternalAccountCredentialsTest.validateMetricsHeader(headers, "aws", true, true); } } @@ -246,7 +255,7 @@ void refreshAccessTokenProgrammaticRefresh_withServiceAccountImpersonation() thr transportFactory.transport.setExpireTime(TestUtils.getDefaultExpireTime()); - AwsSecurityCredentialsSupplier supplier = + TestAwsSecurityCredentialsSupplier supplier = new TestAwsSecurityCredentialsSupplier("test", programmaticAwsCreds, null, null); AwsCredentials awsCredential = @@ -264,11 +273,17 @@ void refreshAccessTokenProgrammaticRefresh_withServiceAccountImpersonation() thr assertEquals( transportFactory.transport.getServiceAccountAccessToken(), accessToken.getTokenValue()); + assertEquals(1, supplier.getRegionCount()); + assertEquals(1, supplier.getCredentialsCount()); // Validate metrics header is set correctly on the sts request. Map> headers = transportFactory.transport.getRequests().get(0).getHeaders(); ExternalAccountCredentialsTest.validateMetricsHeader(headers, "programmatic", true, false); + + awsCredential.refreshAccessToken(); + assertEquals(1, supplier.getRegionCount()); + assertEquals(1, supplier.getCredentialsCount()); } @Test @@ -1353,6 +1368,8 @@ static class TestAwsSecurityCredentialsSupplier implements AwsSecurityCredential private final AwsSecurityCredentials credentials; private final IOException credentialException; private final ExternalAccountSupplierContext expectedContext; + private int getRegionCount = 0; + private int getCredentialsCount = 0; TestAwsSecurityCredentialsSupplier( String region, @@ -1365,8 +1382,17 @@ static class TestAwsSecurityCredentialsSupplier implements AwsSecurityCredential this.expectedContext = expectedContext; } + int getRegionCount() { + return getRegionCount; + } + + int getCredentialsCount() { + return getCredentialsCount; + } + @Override public String getRegion(ExternalAccountSupplierContext context) throws IOException { + getRegionCount++; if (expectedContext != null) { assertEquals(expectedContext.getAudience(), context.getAudience()); assertEquals(expectedContext.getSubjectTokenType(), context.getSubjectTokenType()); @@ -1377,6 +1403,7 @@ public String getRegion(ExternalAccountSupplierContext context) throws IOExcepti @Override public AwsSecurityCredentials getCredentials(ExternalAccountSupplierContext context) throws IOException { + getCredentialsCount++; if (credentialException != null) { throw credentialException; }