From 56336b217387973f99fee7a077fb423dedd8cce8 Mon Sep 17 00:00:00 2001 From: Wes Tarle Date: Fri, 12 Jun 2026 15:43:34 +0000 Subject: [PATCH] test(auth): add caching call-count assertions and fix mock expiration bug Other client libraries (such as Rust and .NET) safely verify caching logic by asserting exactly one outbound HTTP request is made regardless of the number of credential calls. Added `getRequestMetadata_multipleCalls_usesCachedToken` to enforce this. Adding this test exposed a long-standing bug in the test framework: `MockMetadataServerTransport` was erroneously returning `expires_in` as 3,600,000 (mistakenly assuming it was milliseconds instead of seconds). This caused a 32-bit integer overflow in the parser (`expiresInSeconds * 1000` > 2.14B), which instantly expired the mock token and silently bypassed the cache during tests. Fixed the mock to correctly return 3600 seconds, and defensively promoted the production code parser multiplication to a 64-bit `long` to prevent any potential future overflows. --- .../auth/oauth2/ComputeEngineCredentials.java | 2 +- .../google/auth/oauth2/UserCredentials.java | 2 +- .../oauth2/ComputeEngineCredentialsTest.java | 28 +++++++++++++++++++ .../oauth2/MockMetadataServerTransport.java | 2 +- 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java index ad5fb8e7dcf3..9ce3192c6537 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/ComputeEngineCredentials.java @@ -453,7 +453,7 @@ public AccessToken refreshAccessToken() throws IOException { OAuth2Utils.validateString(responseData, "access_token", PARSE_ERROR_PREFIX); int expiresInSeconds = OAuth2Utils.validateInt32(responseData, "expires_in", PARSE_ERROR_PREFIX); - long expiresAtMilliseconds = clock.currentTimeMillis() + expiresInSeconds * 1000; + long expiresAtMilliseconds = clock.currentTimeMillis() + expiresInSeconds * 1000L; return new AccessToken(accessToken, new Date(expiresAtMilliseconds)); } diff --git a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/UserCredentials.java b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/UserCredentials.java index 3670ac7a6804..170adc8ce7df 100644 --- a/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/UserCredentials.java +++ b/google-auth-library-java/oauth2_http/java/com/google/auth/oauth2/UserCredentials.java @@ -193,7 +193,7 @@ public AccessToken refreshAccessToken() throws IOException { OAuth2Utils.validateString(responseData, "access_token", PARSE_ERROR_PREFIX); int expiresInSeconds = OAuth2Utils.validateInt32(responseData, "expires_in", PARSE_ERROR_PREFIX); - long expiresAtMilliseconds = clock.currentTimeMillis() + expiresInSeconds * 1000; + long expiresAtMilliseconds = clock.currentTimeMillis() + expiresInSeconds * 1000L; String scopes = OAuth2Utils.validateOptionalString( responseData, OAuth2Utils.TOKEN_RESPONSE_SCOPE, PARSE_ERROR_PREFIX); diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java index 82240171d9af..09059eadc400 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ComputeEngineCredentialsTest.java @@ -419,6 +419,34 @@ void getRequestMetadata_shouldInvalidateAccessTokenWhenScoped_newAccessTokenFrom TestUtils.assertNotContainsBearerToken(metadataForCopiedCredentials, ACCESS_TOKEN); } + @Test + void getRequestMetadata_multipleCalls_usesCachedToken() throws IOException { + final int[] requestCount = new int[1]; + MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); + transportFactory.transport = + new MockMetadataServerTransport(SCOPE_TO_ACCESS_TOKEN_MAP) { + @Override + public LowLevelHttpRequest buildRequest(String method, String url) throws IOException { + if (url.startsWith(ComputeEngineCredentials.getTokenServerEncodedUrl())) { + requestCount[0]++; + } + return super.buildRequest(method, url); + } + }; + transportFactory.transport.setServiceAccountEmail(SA_CLIENT_EMAIL); + + ComputeEngineCredentials credentials = + ComputeEngineCredentials.newBuilder().setHttpTransportFactory(transportFactory).build(); + + Map> metadata = credentials.getRequestMetadata(CALL_URI); + TestUtils.assertContainsBearerToken(metadata, ACCESS_TOKEN); + assertEquals(1, requestCount[0]); + + Map> metadata2 = credentials.getRequestMetadata(CALL_URI); + TestUtils.assertContainsBearerToken(metadata2, ACCESS_TOKEN); + assertEquals(1, requestCount[0]); + } + @Test void getRequestMetadata_missingServiceAccount_throws() { MockMetadataServerTransportFactory transportFactory = new MockMetadataServerTransportFactory(); diff --git a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java index 1b218b73ef45..e90c6057f324 100644 --- a/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java +++ b/google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MockMetadataServerTransport.java @@ -213,7 +213,7 @@ public LowLevelHttpResponse execute() throws IOException { refreshContents.put( "access_token", scopesToAccessToken.get("[" + urlParsed.get(1) + "]")); } - refreshContents.put("expires_in", 3600000); + refreshContents.put("expires_in", 3600); refreshContents.put("token_type", "Bearer"); String refreshText = refreshContents.toPrettyString();