Skip to content

Commit 59bf06d

Browse files
committed
test: confirm behavior of refresh timeouts
1 parent a65c22d commit 59bf06d

File tree

2 files changed

+299
-0
lines changed

2 files changed

+299
-0
lines changed
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
*
15+
* * Neither the name of Google LLC nor the names of its
16+
* contributors may be used to endorse or promote products derived from
17+
* this software without specific prior written permission.
18+
*
19+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT of THE USE
29+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30+
*/
31+
32+
package com.google.auth.oauth2;
33+
34+
import static org.junit.Assert.assertEquals;
35+
import static org.junit.Assert.assertNotNull;
36+
import static org.junit.Assert.assertThrows;
37+
import static org.junit.Assert.assertTrue;
38+
39+
import com.google.api.client.http.javanet.NetHttpTransport;
40+
import com.google.api.client.json.GenericJson;
41+
import com.google.auth.http.HttpTransportFactory;
42+
import java.io.IOException;
43+
import java.net.HttpURLConnection;
44+
import java.net.InetAddress;
45+
import java.net.SocketTimeoutException;
46+
import java.security.GeneralSecurityException;
47+
import java.security.SecureRandom;
48+
import java.security.cert.X509Certificate;
49+
import java.text.SimpleDateFormat;
50+
import java.util.Calendar;
51+
import java.util.Collections;
52+
import java.util.Date;
53+
import java.util.concurrent.TimeUnit;
54+
import javax.net.ssl.HostnameVerifier;
55+
import javax.net.ssl.SSLContext;
56+
import javax.net.ssl.SSLSocketFactory;
57+
import javax.net.ssl.TrustManager;
58+
import javax.net.ssl.X509TrustManager;
59+
import okhttp3.mockwebserver.MockResponse;
60+
import okhttp3.mockwebserver.MockWebServer;
61+
import okhttp3.tls.HandshakeCertificates;
62+
import okhttp3.tls.HeldCertificate;
63+
import org.junit.Test;
64+
65+
public class CredentialsTimeoutTest {
66+
67+
private static final int TIMEOUT_MS = 1000; // 1 second
68+
69+
@Test
70+
public void externalAccount_shouldTimeout() throws Exception {
71+
// Create a server that is slow on the second (access token) request.
72+
MockWebServer server = createExternalAccountMockServer(0, 2000);
73+
server.start();
74+
75+
String tokenUrl = server.url("/token").toString();
76+
String subjectTokenUrl = server.url("/subject_token").toString();
77+
78+
HttpTransportFactory transportFactory = createTimeoutTransportFactory(TIMEOUT_MS);
79+
ExternalAccountCredentials credentials =
80+
createExternalAccountCredentials(tokenUrl, subjectTokenUrl, transportFactory);
81+
82+
IOException exception = assertThrows(IOException.class, credentials::refresh);
83+
assertTrue(exception.getCause() instanceof SocketTimeoutException);
84+
85+
server.shutdown();
86+
}
87+
88+
@Test
89+
public void externalAccount_shouldNotTimeout() throws Exception {
90+
// Create a server that is faster than the client's timeout.
91+
MockWebServer server = createExternalAccountMockServer(0, 500);
92+
server.start();
93+
94+
String tokenUrl = server.url("/token").toString();
95+
String subjectTokenUrl = server.url("/subject_token").toString();
96+
97+
HttpTransportFactory transportFactory = createTimeoutTransportFactory(TIMEOUT_MS);
98+
ExternalAccountCredentials credentials =
99+
createExternalAccountCredentials(tokenUrl, subjectTokenUrl, transportFactory);
100+
101+
credentials.refresh();
102+
assertNotNull(credentials.getAccessToken());
103+
assertEquals("test_token", credentials.getAccessToken().getTokenValue());
104+
105+
server.shutdown();
106+
}
107+
108+
@Test
109+
public void impersonatedCredentials_shouldTimeout() throws Exception {
110+
// Create a server that is slower than the client's timeout.
111+
MockWebServer server = createImpersonatedMockServer(2000);
112+
server.start();
113+
114+
String iamEndpoint = server.url("/").toString();
115+
116+
HttpTransportFactory transportFactory = createTimeoutTransportFactory(TIMEOUT_MS);
117+
ImpersonatedCredentials credentials =
118+
createImpersonatedCredentials(iamEndpoint, transportFactory);
119+
120+
IOException exception = assertThrows(IOException.class, credentials::refresh);
121+
assertTrue(exception.getCause() instanceof SocketTimeoutException);
122+
123+
server.shutdown();
124+
}
125+
126+
@Test
127+
public void impersonatedCredentials_shouldNotTimeout() throws Exception {
128+
// Create a server that is faster than the client's timeout.
129+
MockWebServer server = createImpersonatedMockServer(500);
130+
server.start();
131+
132+
String iamEndpoint = server.url("/").toString();
133+
134+
HttpTransportFactory transportFactory = createTimeoutTransportFactory(TIMEOUT_MS);
135+
ImpersonatedCredentials credentials =
136+
createImpersonatedCredentials(iamEndpoint, transportFactory);
137+
138+
credentials.refresh();
139+
assertNotNull(credentials.getAccessToken());
140+
assertEquals("impersonated-token", credentials.getAccessToken().getTokenValue());
141+
142+
server.shutdown();
143+
}
144+
145+
// Server and Credential Helper Methods
146+
147+
private static MockWebServer createExternalAccountMockServer(
148+
int subjectTokenDelayMs, int accessTokenDelayMs) throws IOException {
149+
MockWebServer server = new MockWebServer();
150+
configureServerForHttps(server);
151+
152+
server.enqueue(
153+
new MockResponse()
154+
.setBody("dummy-subject-token")
155+
.setBodyDelay(subjectTokenDelayMs, TimeUnit.MILLISECONDS));
156+
157+
String tokenResponse =
158+
"{"
159+
+ "\"access_token\": \"test_token\","
160+
+ "\"issued_token_type\": \"urn:ietf:params:oauth:token-type:access_token\","
161+
+ "\"token_type\": \"Bearer\","
162+
+ "\"expires_in\": 3600"
163+
+ "}";
164+
server.enqueue(
165+
new MockResponse()
166+
.setBody(tokenResponse)
167+
.setBodyDelay(accessTokenDelayMs, TimeUnit.MILLISECONDS));
168+
169+
return server;
170+
}
171+
172+
private static String getDefaultExpireTime() {
173+
Calendar calendar = Calendar.getInstance();
174+
calendar.setTime(new Date());
175+
calendar.add(Calendar.SECOND, 300);
176+
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").format(calendar.getTime());
177+
}
178+
179+
private static MockWebServer createImpersonatedMockServer(int delayMs) throws IOException {
180+
MockWebServer server = new MockWebServer();
181+
configureServerForHttps(server);
182+
183+
String expireTime = getDefaultExpireTime();
184+
String tokenResponse =
185+
"{"
186+
+ "\"accessToken\": \"impersonated-token\","
187+
+ "\"expireTime\": \""
188+
+ expireTime
189+
+ "\""
190+
+ "}";
191+
server.enqueue(
192+
new MockResponse().setBody(tokenResponse).setBodyDelay(delayMs, TimeUnit.MILLISECONDS));
193+
194+
return server;
195+
}
196+
197+
private static void configureServerForHttps(MockWebServer server) throws IOException {
198+
String localhost = InetAddress.getByName("localhost").getCanonicalHostName();
199+
HeldCertificate localhostCertificate =
200+
new HeldCertificate.Builder().addSubjectAlternativeName(localhost).build();
201+
HandshakeCertificates serverCertificates =
202+
new HandshakeCertificates.Builder().heldCertificate(localhostCertificate).build();
203+
server.useHttps(serverCertificates.sslSocketFactory(), false);
204+
}
205+
206+
private static ExternalAccountCredentials createExternalAccountCredentials(
207+
String tokenUrl, String subjectTokenUrl, HttpTransportFactory transportFactory)
208+
throws IOException {
209+
GenericJson credentialSource = new GenericJson();
210+
credentialSource.put("url", subjectTokenUrl);
211+
credentialSource.put("headers", Collections.singletonMap("Metadata-Flavor", "Google"));
212+
213+
GenericJson json = new GenericJson();
214+
json.put("type", "external_account");
215+
json.put(
216+
"audience",
217+
"//iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/pool/providers/provider");
218+
json.put("subject_token_type", "urn:ietf:params:oauth:token-type:jwt");
219+
json.put("token_url", tokenUrl);
220+
json.put("credential_source", credentialSource);
221+
222+
return ExternalAccountCredentials.fromJson(json, transportFactory);
223+
}
224+
225+
private static ImpersonatedCredentials createImpersonatedCredentials(
226+
String iamEndpoint, HttpTransportFactory transportFactory) {
227+
GoogleCredentials source =
228+
new GoogleCredentials(new AccessToken("dummy-token", new Date())) {
229+
@Override
230+
public AccessToken refreshAccessToken() throws IOException {
231+
// In a real scenario, this would fetch a new token. For this test,
232+
// we just return a dummy token since the impersonation flow will
233+
// use this credential's metadata, not its token.
234+
return new AccessToken("refreshed-dummy-token", new Date());
235+
}
236+
};
237+
238+
return ImpersonatedCredentials.newBuilder()
239+
.setSourceCredentials(source)
240+
.setTargetPrincipal("[email protected]")
241+
.setScopes(Collections.singletonList("https://www.googleapis.com/auth/cloud-platform"))
242+
.setIamEndpointOverride(iamEndpoint)
243+
.setHttpTransportFactory(transportFactory)
244+
.build();
245+
}
246+
247+
private static HttpTransportFactory createTimeoutTransportFactory(int timeoutMs) {
248+
return () -> {
249+
TrustManager[] trustAllCerts =
250+
new TrustManager[] {
251+
new X509TrustManager() {
252+
public X509Certificate[] getAcceptedIssuers() {
253+
return new X509Certificate[0];
254+
}
255+
256+
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
257+
258+
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
259+
}
260+
};
261+
262+
SSLContext sslContext;
263+
try {
264+
sslContext = SSLContext.getInstance("TLS");
265+
sslContext.init(null, trustAllCerts, new SecureRandom());
266+
} catch (GeneralSecurityException e) {
267+
throw new RuntimeException(e);
268+
}
269+
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
270+
HostnameVerifier hostnameVerifier = (hostname, session) -> true;
271+
272+
NetHttpTransport.Builder builder =
273+
new NetHttpTransport.Builder()
274+
.setSslSocketFactory(sslSocketFactory)
275+
.setHostnameVerifier(hostnameVerifier);
276+
277+
builder.setConnectionFactory(
278+
url -> {
279+
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
280+
connection.setConnectTimeout(timeoutMs);
281+
connection.setReadTimeout(timeoutMs);
282+
return connection;
283+
});
284+
return builder.build();
285+
};
286+
}
287+
}

oauth2_http/pom.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,18 @@
329329
<groupId>com.google.api</groupId>
330330
<artifactId>api-common</artifactId>
331331
</dependency>
332+
<dependency>
333+
<groupId>com.squareup.okhttp3</groupId>
334+
<artifactId>mockwebserver</artifactId>
335+
<version>5.3.2</version>
336+
<scope>test</scope>
337+
</dependency>
338+
<dependency>
339+
<groupId>com.squareup.okhttp3</groupId>
340+
<artifactId>okhttp-tls</artifactId>
341+
<version>5.3.2</version>
342+
<scope>test</scope>
343+
</dependency>
332344
<dependency>
333345
<groupId>junit</groupId>
334346
<artifactId>junit</artifactId>

0 commit comments

Comments
 (0)