From 59bf06db764d6407aba0429ef6aaf5d72791b7a9 Mon Sep 17 00:00:00 2001 From: Diego Alonso Marquez Palacios Date: Fri, 28 Nov 2025 17:33:28 -0500 Subject: [PATCH] test: confirm behavior of refresh timeouts --- .../auth/oauth2/CredentialsTimeoutTest.java | 287 ++++++++++++++++++ oauth2_http/pom.xml | 12 + 2 files changed, 299 insertions(+) create mode 100644 oauth2_http/javatests/com/google/auth/oauth2/CredentialsTimeoutTest.java diff --git a/oauth2_http/javatests/com/google/auth/oauth2/CredentialsTimeoutTest.java b/oauth2_http/javatests/com/google/auth/oauth2/CredentialsTimeoutTest.java new file mode 100644 index 000000000..0a6d0b2a2 --- /dev/null +++ b/oauth2_http/javatests/com/google/auth/oauth2/CredentialsTimeoutTest.java @@ -0,0 +1,287 @@ +/* + * Copyright 2025 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT of THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.google.auth.oauth2; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.GenericJson; +import com.google.auth.http.HttpTransportFactory; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.SocketTimeoutException; +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.concurrent.TimeUnit; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.tls.HandshakeCertificates; +import okhttp3.tls.HeldCertificate; +import org.junit.Test; + +public class CredentialsTimeoutTest { + + private static final int TIMEOUT_MS = 1000; // 1 second + + @Test + public void externalAccount_shouldTimeout() throws Exception { + // Create a server that is slow on the second (access token) request. + MockWebServer server = createExternalAccountMockServer(0, 2000); + server.start(); + + String tokenUrl = server.url("/token").toString(); + String subjectTokenUrl = server.url("/subject_token").toString(); + + HttpTransportFactory transportFactory = createTimeoutTransportFactory(TIMEOUT_MS); + ExternalAccountCredentials credentials = + createExternalAccountCredentials(tokenUrl, subjectTokenUrl, transportFactory); + + IOException exception = assertThrows(IOException.class, credentials::refresh); + assertTrue(exception.getCause() instanceof SocketTimeoutException); + + server.shutdown(); + } + + @Test + public void externalAccount_shouldNotTimeout() throws Exception { + // Create a server that is faster than the client's timeout. + MockWebServer server = createExternalAccountMockServer(0, 500); + server.start(); + + String tokenUrl = server.url("/token").toString(); + String subjectTokenUrl = server.url("/subject_token").toString(); + + HttpTransportFactory transportFactory = createTimeoutTransportFactory(TIMEOUT_MS); + ExternalAccountCredentials credentials = + createExternalAccountCredentials(tokenUrl, subjectTokenUrl, transportFactory); + + credentials.refresh(); + assertNotNull(credentials.getAccessToken()); + assertEquals("test_token", credentials.getAccessToken().getTokenValue()); + + server.shutdown(); + } + + @Test + public void impersonatedCredentials_shouldTimeout() throws Exception { + // Create a server that is slower than the client's timeout. + MockWebServer server = createImpersonatedMockServer(2000); + server.start(); + + String iamEndpoint = server.url("/").toString(); + + HttpTransportFactory transportFactory = createTimeoutTransportFactory(TIMEOUT_MS); + ImpersonatedCredentials credentials = + createImpersonatedCredentials(iamEndpoint, transportFactory); + + IOException exception = assertThrows(IOException.class, credentials::refresh); + assertTrue(exception.getCause() instanceof SocketTimeoutException); + + server.shutdown(); + } + + @Test + public void impersonatedCredentials_shouldNotTimeout() throws Exception { + // Create a server that is faster than the client's timeout. + MockWebServer server = createImpersonatedMockServer(500); + server.start(); + + String iamEndpoint = server.url("/").toString(); + + HttpTransportFactory transportFactory = createTimeoutTransportFactory(TIMEOUT_MS); + ImpersonatedCredentials credentials = + createImpersonatedCredentials(iamEndpoint, transportFactory); + + credentials.refresh(); + assertNotNull(credentials.getAccessToken()); + assertEquals("impersonated-token", credentials.getAccessToken().getTokenValue()); + + server.shutdown(); + } + + // Server and Credential Helper Methods + + private static MockWebServer createExternalAccountMockServer( + int subjectTokenDelayMs, int accessTokenDelayMs) throws IOException { + MockWebServer server = new MockWebServer(); + configureServerForHttps(server); + + server.enqueue( + new MockResponse() + .setBody("dummy-subject-token") + .setBodyDelay(subjectTokenDelayMs, TimeUnit.MILLISECONDS)); + + String tokenResponse = + "{" + + "\"access_token\": \"test_token\"," + + "\"issued_token_type\": \"urn:ietf:params:oauth:token-type:access_token\"," + + "\"token_type\": \"Bearer\"," + + "\"expires_in\": 3600" + + "}"; + server.enqueue( + new MockResponse() + .setBody(tokenResponse) + .setBodyDelay(accessTokenDelayMs, TimeUnit.MILLISECONDS)); + + return server; + } + + private static String getDefaultExpireTime() { + Calendar calendar = Calendar.getInstance(); + calendar.setTime(new Date()); + calendar.add(Calendar.SECOND, 300); + return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(calendar.getTime()); + } + + private static MockWebServer createImpersonatedMockServer(int delayMs) throws IOException { + MockWebServer server = new MockWebServer(); + configureServerForHttps(server); + + String expireTime = getDefaultExpireTime(); + String tokenResponse = + "{" + + "\"accessToken\": \"impersonated-token\"," + + "\"expireTime\": \"" + + expireTime + + "\"" + + "}"; + server.enqueue( + new MockResponse().setBody(tokenResponse).setBodyDelay(delayMs, TimeUnit.MILLISECONDS)); + + return server; + } + + private static void configureServerForHttps(MockWebServer server) throws IOException { + String localhost = InetAddress.getByName("localhost").getCanonicalHostName(); + HeldCertificate localhostCertificate = + new HeldCertificate.Builder().addSubjectAlternativeName(localhost).build(); + HandshakeCertificates serverCertificates = + new HandshakeCertificates.Builder().heldCertificate(localhostCertificate).build(); + server.useHttps(serverCertificates.sslSocketFactory(), false); + } + + private static ExternalAccountCredentials createExternalAccountCredentials( + String tokenUrl, String subjectTokenUrl, HttpTransportFactory transportFactory) + throws IOException { + GenericJson credentialSource = new GenericJson(); + credentialSource.put("url", subjectTokenUrl); + credentialSource.put("headers", Collections.singletonMap("Metadata-Flavor", "Google")); + + GenericJson json = new GenericJson(); + json.put("type", "external_account"); + json.put( + "audience", + "//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider"); + json.put("subject_token_type", "urn:ietf:params:oauth:token-type:jwt"); + json.put("token_url", tokenUrl); + json.put("credential_source", credentialSource); + + return ExternalAccountCredentials.fromJson(json, transportFactory); + } + + private static ImpersonatedCredentials createImpersonatedCredentials( + String iamEndpoint, HttpTransportFactory transportFactory) { + GoogleCredentials source = + new GoogleCredentials(new AccessToken("dummy-token", new Date())) { + @Override + public AccessToken refreshAccessToken() throws IOException { + // In a real scenario, this would fetch a new token. For this test, + // we just return a dummy token since the impersonation flow will + // use this credential's metadata, not its token. + return new AccessToken("refreshed-dummy-token", new Date()); + } + }; + + return ImpersonatedCredentials.newBuilder() + .setSourceCredentials(source) + .setTargetPrincipal("test-sa@example.iam.gserviceaccount.com") + .setScopes(Collections.singletonList("https://www.googleapis.com/auth/cloud-platform")) + .setIamEndpointOverride(iamEndpoint) + .setHttpTransportFactory(transportFactory) + .build(); + } + + private static HttpTransportFactory createTimeoutTransportFactory(int timeoutMs) { + return () -> { + TrustManager[] trustAllCerts = + new TrustManager[] { + new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + public void checkClientTrusted(X509Certificate[] certs, String authType) {} + + public void checkServerTrusted(X509Certificate[] certs, String authType) {} + } + }; + + SSLContext sslContext; + try { + sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustAllCerts, new SecureRandom()); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } + SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory(); + HostnameVerifier hostnameVerifier = (hostname, session) -> true; + + NetHttpTransport.Builder builder = + new NetHttpTransport.Builder() + .setSslSocketFactory(sslSocketFactory) + .setHostnameVerifier(hostnameVerifier); + + builder.setConnectionFactory( + url -> { + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setConnectTimeout(timeoutMs); + connection.setReadTimeout(timeoutMs); + return connection; + }); + return builder.build(); + }; + } +} diff --git a/oauth2_http/pom.xml b/oauth2_http/pom.xml index 42e81f2c5..ecf34feda 100644 --- a/oauth2_http/pom.xml +++ b/oauth2_http/pom.xml @@ -329,6 +329,18 @@ com.google.api api-common + + com.squareup.okhttp3 + mockwebserver + 5.3.2 + test + + + com.squareup.okhttp3 + okhttp-tls + 5.3.2 + test + junit junit