Skip to content

Commit 266002c

Browse files
committed
test(oauth2): refine end-to-end mTLS cert rotation integration test with static fixtures and SSLServerSocket
1 parent 58d43fc commit 266002c

7 files changed

Lines changed: 256 additions & 140 deletions

File tree

google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/MtlsCertRotationIntegrationTest.java renamed to google-auth-library-java/oauth2_http/javatests/com/google/auth/oauth2/ITMtlsCertRotationTest.java

Lines changed: 115 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
1919
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
2020
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21-
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21+
* A PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
2222
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
2323
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
2424
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
@@ -37,21 +37,18 @@
3737
import com.google.api.client.json.GenericJson;
3838
import com.google.auth.mtls.MtlsHttpTransportFactory;
3939
import com.google.auth.mtls.X509Provider;
40-
import com.sun.net.httpserver.HttpExchange;
41-
import com.sun.net.httpserver.HttpHandler;
42-
import com.sun.net.httpserver.HttpsConfigurator;
43-
import com.sun.net.httpserver.HttpsExchange;
44-
import com.sun.net.httpserver.HttpsParameters;
45-
import com.sun.net.httpserver.HttpsServer;
40+
import java.io.BufferedReader;
41+
import java.io.File;
4642
import java.io.FileInputStream;
4743
import java.io.FileOutputStream;
48-
import java.io.IOException;
4944
import java.io.InputStream;
45+
import java.io.InputStreamReader;
5046
import java.io.OutputStream;
5147
import java.net.InetSocketAddress;
5248
import java.nio.charset.StandardCharsets;
5349
import java.nio.file.Files;
5450
import java.nio.file.Path;
51+
import java.nio.file.Paths;
5552
import java.nio.file.StandardCopyOption;
5653
import java.security.KeyFactory;
5754
import java.security.KeyStore;
@@ -66,30 +63,40 @@
6663
import java.util.HashMap;
6764
import java.util.List;
6865
import java.util.Map;
66+
import java.util.concurrent.CountDownLatch;
67+
import java.util.concurrent.TimeUnit;
6968
import java.util.concurrent.atomic.AtomicInteger;
7069
import javax.net.ssl.KeyManagerFactory;
7170
import javax.net.ssl.SSLContext;
72-
import javax.net.ssl.SSLEngine;
73-
import javax.net.ssl.SSLParameters;
71+
import javax.net.ssl.SSLServerSocket;
72+
import javax.net.ssl.SSLSocket;
7473
import javax.net.ssl.TrustManagerFactory;
7574
import org.junit.jupiter.api.AfterEach;
7675
import org.junit.jupiter.api.BeforeEach;
7776
import org.junit.jupiter.api.Test;
7877
import org.junit.jupiter.api.io.TempDir;
7978

8079
/**
81-
* End-to-end integration and manual verification test infrastructure for:
80+
* End-to-end integration test verifying:
8281
* 1) mTLS Dynamic Certificate Rotation (MtlsHttpTransportFactory + X509Provider)
83-
* 2) STS 401 Unauthorized Retry Loop over real local HTTPS sockets requiring mTLS
82+
* 2) STS 401 Unauthorized Retry Interception over real local HTTPS sockets requiring mTLS
83+
*
84+
* Uses raw SSLServerSocket and static pre-generated test resource fixtures in testresources/mtls/
85+
* for zero host process / OpenSSL dependencies and zero JDK module instrumentation warnings.
8486
*/
85-
public class MtlsCertRotationIntegrationTest {
87+
public class ITMtlsCertRotationTest {
88+
89+
private static final String TESTRESOURCES_DIR = "testresources/mtls/";
8690

8791
@TempDir Path tempDir;
8892

89-
private HttpsServer server;
93+
private SSLServerSocket serverSocket;
94+
private Thread serverThread;
95+
private volatile boolean running = true;
9096
private int serverPort;
9197
private final List<String> peerCertificatesReceived = Collections.synchronizedList(new ArrayList<>());
9298
private final AtomicInteger requestCounter = new AtomicInteger(0);
99+
private final CountDownLatch serverReadyLatch = new CountDownLatch(1);
93100

94101
private Path certConfigPath;
95102
private Path activeCertPath;
@@ -98,12 +105,19 @@ public class MtlsCertRotationIntegrationTest {
98105
private Path key1Path;
99106
private Path cert2Path;
100107
private Path key2Path;
108+
private Path serverCertPath;
109+
private Path serverKeyPath;
101110
private String oldTrustStore;
102111
private String oldTrustStorePassword;
103112

104113
@BeforeEach
105114
void setUp() throws Exception {
106-
generateCertificates();
115+
cert1Path = Paths.get(TESTRESOURCES_DIR, "client_v1.crt");
116+
key1Path = Paths.get(TESTRESOURCES_DIR, "client_v1.pem.key");
117+
cert2Path = Paths.get(TESTRESOURCES_DIR, "client_v2.crt");
118+
key2Path = Paths.get(TESTRESOURCES_DIR, "client_v2.pem.key");
119+
serverCertPath = Paths.get(TESTRESOURCES_DIR, "server.crt");
120+
serverKeyPath = Paths.get(TESTRESOURCES_DIR, "server.pem.key");
107121

108122
activeCertPath = tempDir.resolve("active_client.crt");
109123
activeKeyPath = tempDir.resolve("active_client.pem.key");
@@ -122,28 +136,31 @@ void setUp() throws Exception {
122136
+ "}\n";
123137
Files.write(certConfigPath, configJson.getBytes(StandardCharsets.UTF_8));
124138

125-
// Save previous truststore properties and set to our temporary client truststore
126139
oldTrustStore = System.getProperty("javax.net.ssl.trustStore");
127140
oldTrustStorePassword = System.getProperty("javax.net.ssl.trustStorePassword");
128141

129142
Path clientTrustStorePath = tempDir.resolve("client_truststore.p12");
130143
KeyStore clientTrustStore = KeyStore.getInstance("PKCS12");
131144
clientTrustStore.load(null, null);
132-
addCertToTrustStore(clientTrustStore, tempDir.resolve("server.crt"), "server");
145+
addCertToTrustStore(clientTrustStore, serverCertPath, "server");
133146
try (FileOutputStream fos = new FileOutputStream(clientTrustStorePath.toFile())) {
134147
clientTrustStore.store(fos, "password".toCharArray());
135148
}
136149

137150
System.setProperty("javax.net.ssl.trustStore", clientTrustStorePath.toString());
138151
System.setProperty("javax.net.ssl.trustStorePassword", "password");
139152

140-
startLocalMtlsServer();
153+
startLocalMtlsServerSocket();
141154
}
142155

143156
@AfterEach
144157
void tearDown() {
145-
if (server != null) {
146-
server.stop(0);
158+
running = false;
159+
if (serverSocket != null && !serverSocket.isClosed()) {
160+
try {
161+
serverSocket.close();
162+
} catch (Exception ignored) {
163+
}
147164
}
148165
if (oldTrustStore != null) {
149166
System.setProperty("javax.net.ssl.trustStore", oldTrustStore);
@@ -159,7 +176,7 @@ void tearDown() {
159176

160177
@Test
161178
void endToEndMtlsCertRotation_on401Retry_reloadsRotatedCertAndSucceeds() throws Exception {
162-
System.out.println("=== Starting End-to-End mTLS Certificate Rotation Integration Test ===");
179+
assertTrue(serverReadyLatch.await(5, TimeUnit.SECONDS), "Server socket failed to start in time");
163180

164181
X509Provider x509Provider = new X509Provider(certConfigPath.toString());
165182
MtlsHttpTransportFactory transportFactory = new MtlsHttpTransportFactory(x509Provider);
@@ -193,133 +210,91 @@ void endToEndMtlsCertRotation_on401Retry_reloadsRotatedCertAndSucceeds() throws
193210

194211
assertTrue(peerCertificatesReceived.get(0).contains("CN=client-v1"));
195212
assertTrue(peerCertificatesReceived.get(1).contains("CN=client-v2"));
196-
197-
System.out.println("=== Verified: Cert V1 -> 401 -> Cert Rotation -> Cert V2 -> 200 OK Token Received! ===");
198-
}
199-
200-
private void generateCertificates() throws Exception {
201-
cert1Path = tempDir.resolve("cert1.crt");
202-
key1Path = tempDir.resolve("cert1.pem.key");
203-
cert2Path = tempDir.resolve("cert2.crt");
204-
key2Path = tempDir.resolve("cert2.pem.key");
205-
Path serverCertPath = tempDir.resolve("server.crt");
206-
Path serverKeyPath = tempDir.resolve("server.pem.key");
207-
208-
runOpenSslCommandWithSan(serverKeyPath, serverCertPath, "/CN=127.0.0.1", "subjectAltName=IP:127.0.0.1,DNS:localhost");
209-
runOpenSslCommand(key1Path, cert1Path, "/CN=client-v1");
210-
runOpenSslCommand(key2Path, cert2Path, "/CN=client-v2");
211-
}
212-
213-
private void runOpenSslCommand(Path keyOut, Path certOut, String subj) throws Exception {
214-
runOpenSslCommandWithSan(keyOut, certOut, subj, null);
215-
}
216-
217-
private void runOpenSslCommandWithSan(Path keyOut, Path certOut, String subj, String sanExt) throws Exception {
218-
List<String> cmd = new ArrayList<>();
219-
cmd.add("openssl");
220-
cmd.add("req");
221-
cmd.add("-x509");
222-
cmd.add("-newkey");
223-
cmd.add("rsa:2048");
224-
cmd.add("-keyout");
225-
cmd.add(keyOut.toString());
226-
cmd.add("-out");
227-
cmd.add(certOut.toString());
228-
cmd.add("-days");
229-
cmd.add("1");
230-
cmd.add("-nodes");
231-
cmd.add("-subj");
232-
cmd.add(subj);
233-
if (sanExt != null) {
234-
cmd.add("-addext");
235-
cmd.add(sanExt);
236-
}
237-
ProcessBuilder pb = new ProcessBuilder(cmd);
238-
int exitCode = pb.start().waitFor();
239-
if (exitCode != 0) {
240-
throw new RuntimeException("OpenSSL cert generation failed for " + subj);
241-
}
242213
}
243214

244-
private void startLocalMtlsServer() throws Exception {
245-
server = HttpsServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
246-
serverPort = server.getAddress().getPort();
247-
215+
private void startLocalMtlsServerSocket() throws Exception {
248216
SSLContext serverSslContext = createServerSslContext();
249-
server.setHttpsConfigurator(
250-
new HttpsConfigurator(serverSslContext) {
251-
@Override
252-
public void configure(HttpsParameters params) {
253-
SSLEngine engine = serverSslContext.createSSLEngine();
254-
SSLParameters sslParams = serverSslContext.getDefaultSSLParameters();
255-
sslParams.setNeedClientAuth(true);
256-
params.setSSLParameters(sslParams);
257-
}
258-
});
259-
260-
server.createContext(
261-
"/sts/token",
262-
new HttpHandler() {
263-
@Override
264-
public void handle(HttpExchange exchange) throws IOException {
265-
int count = requestCounter.incrementAndGet();
266-
String peerPrincipalName = "UNKNOWN";
267-
try {
268-
if (exchange instanceof HttpsExchange) {
269-
Certificate[] certs = ((HttpsExchange) exchange).getSSLSession().getPeerCertificates();
270-
if (certs != null && certs.length > 0 && certs[0] instanceof X509Certificate) {
271-
peerPrincipalName = ((X509Certificate) certs[0]).getSubjectX500Principal().getName();
272-
peerCertificatesReceived.add(peerPrincipalName);
217+
serverSocket = (SSLServerSocket) serverSslContext.getServerSocketFactory().createServerSocket();
218+
serverSocket.bind(new InetSocketAddress("127.0.0.1", 0));
219+
serverSocket.setNeedClientAuth(true);
220+
serverPort = serverSocket.getLocalPort();
221+
222+
serverThread =
223+
new Thread(
224+
() -> {
225+
serverReadyLatch.countDown();
226+
while (running) {
227+
try (SSLSocket clientSocket = (SSLSocket) serverSocket.accept()) {
228+
clientSocket.startHandshake();
229+
int count = requestCounter.incrementAndGet();
230+
String peerPrincipalName = "UNKNOWN";
231+
Certificate[] certs = clientSocket.getSession().getPeerCertificates();
232+
if (certs != null && certs.length > 0 && certs[0] instanceof X509Certificate) {
233+
peerPrincipalName = ((X509Certificate) certs[0]).getSubjectX500Principal().getName();
234+
peerCertificatesReceived.add(peerPrincipalName);
235+
}
236+
237+
BufferedReader reader =
238+
new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), StandardCharsets.UTF_8));
239+
String line;
240+
int contentLength = 0;
241+
while ((line = reader.readLine()) != null && !line.isEmpty()) {
242+
if (line.toLowerCase().startsWith("content-length:")) {
243+
contentLength = Integer.parseInt(line.split(":")[1].trim());
244+
}
245+
}
246+
if (contentLength > 0) {
247+
char[] body = new char[contentLength];
248+
reader.read(body, 0, contentLength);
249+
}
250+
251+
OutputStream os = clientSocket.getOutputStream();
252+
if (peerPrincipalName.contains("client-v1")) {
253+
Path tmpCert = tempDir.resolve("tmp_active_cert.crt");
254+
Path tmpKey = tempDir.resolve("tmp_active_key.pem.key");
255+
Files.copy(cert2Path, tmpCert, StandardCopyOption.REPLACE_EXISTING);
256+
Files.copy(key2Path, tmpKey, StandardCopyOption.REPLACE_EXISTING);
257+
Files.move(tmpCert, activeCertPath, StandardCopyOption.ATOMIC_MOVE);
258+
Files.move(tmpKey, activeKeyPath, StandardCopyOption.ATOMIC_MOVE);
259+
260+
String jsonError =
261+
"{\"error\": \"invalid_grant\", \"error_description\": \"mTLS Certificate Expired\"}";
262+
byte[] payload = jsonError.getBytes(StandardCharsets.UTF_8);
263+
String response =
264+
"HTTP/1.1 401 Unauthorized\r\n"
265+
+ "Content-Type: application/json\r\n"
266+
+ "Content-Length: " + payload.length + "\r\n"
267+
+ "Connection: close\r\n\r\n";
268+
os.write(response.getBytes(StandardCharsets.UTF_8));
269+
os.write(payload);
270+
os.flush();
271+
} else {
272+
String jsonOk =
273+
"{\"access_token\": \"access_token_via_rotated_mtls_cert_v2\","
274+
+ " \"issued_token_type\": \"urn:ietf:params:oauth:token-type:access_token\","
275+
+ " \"token_type\": \"Bearer\", \"expires_in\": 3600}";
276+
byte[] payload = jsonOk.getBytes(StandardCharsets.UTF_8);
277+
String response =
278+
"HTTP/1.1 200 OK\r\n"
279+
+ "Content-Type: application/json\r\n"
280+
+ "Content-Length: " + payload.length + "\r\n"
281+
+ "Connection: close\r\n\r\n";
282+
os.write(response.getBytes(StandardCharsets.UTF_8));
283+
os.write(payload);
284+
os.flush();
285+
}
286+
} catch (Exception e) {
287+
if (running) {
288+
e.printStackTrace();
289+
}
273290
}
274291
}
275-
} catch (Exception e) {
276-
e.printStackTrace();
277-
}
278-
279-
System.out.printf(
280-
"| Server Handler | Request #%d received peer certificate: %s%n",
281-
count, peerPrincipalName);
282-
283-
if (peerPrincipalName.contains("client-v1")) {
284-
try {
285-
Files.copy(cert2Path, activeCertPath, StandardCopyOption.REPLACE_EXISTING);
286-
Files.copy(key2Path, activeKeyPath, StandardCopyOption.REPLACE_EXISTING);
287-
System.out.println(
288-
"| Server Handler | Simulating cert rotation on disk: active cert is now Client Cert V2");
289-
} catch (Exception e) {
290-
e.printStackTrace();
291-
}
292-
293-
String errorResponse =
294-
"{\"error\": \"invalid_grant\", \"error_description\": \"mTLS Certificate Expired\"}";
295-
byte[] bytes = errorResponse.getBytes(StandardCharsets.UTF_8);
296-
exchange.getResponseHeaders().set("Content-Type", "application/json");
297-
exchange.sendResponseHeaders(401, bytes.length);
298-
try (OutputStream os = exchange.getResponseBody()) {
299-
os.write(bytes);
300-
}
301-
} else {
302-
String tokenResponse =
303-
"{\"access_token\": \"access_token_via_rotated_mtls_cert_v2\","
304-
+ " \"issued_token_type\": \"urn:ietf:params:oauth:token-type:access_token\","
305-
+ " \"token_type\": \"Bearer\", \"expires_in\": 3600}";
306-
byte[] bytes = tokenResponse.getBytes(StandardCharsets.UTF_8);
307-
exchange.getResponseHeaders().set("Content-Type", "application/json");
308-
exchange.sendResponseHeaders(200, bytes.length);
309-
try (OutputStream os = exchange.getResponseBody()) {
310-
os.write(bytes);
311-
}
312-
}
313-
}
314-
});
315-
316-
server.start();
292+
});
293+
serverThread.setDaemon(true);
294+
serverThread.start();
317295
}
318296

319297
private SSLContext createServerSslContext() throws Exception {
320-
Path serverCertPath = tempDir.resolve("server.crt");
321-
Path serverKeyPath = tempDir.resolve("server.pem.key");
322-
323298
KeyStore keyStore = createKeyStoreFromPem(serverCertPath, serverKeyPath, "server");
324299
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
325300
kmf.init(keyStore, "password".toCharArray());
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDCTCCAfGgAwIBAgIUcj6ruVvnrnLNhLekHoNMF05pAf4wDQYJKoZIhvcNAQEL
3+
BQAwFDESMBAGA1UEAwwJY2xpZW50LXYxMB4XDTI2MDgxMTAyMzAzM1oXDTM2MDgw
4+
ODAyMzAzM1owFDESMBAGA1UEAwwJY2xpZW50LXYxMIIBIjANBgkqhkiG9w0BAQEF
5+
AAOCAQ8AMIIBCgKCAQEAtvlw2IAfZ2Ad7yJknKKDv7UnocCeGnPoA3FJiFAPue24
6+
R/jkyW7bspYHUB9mfH1FnQklvmay5sShh8+tOreQZ0V5Jls7/YEtYGX5NXMD6wcq
7+
nWxYt63aCTG6JNBORtZBLCFiDyoj+ZPGk2hFXpU+zYu/XXtukNCrivh9MqVjJDlU
8+
EU+g5836xUPt/rq8oQPVNRa5mcW1lU0OTVAvLW0IALKqm7Jb54cCK+YluesOLuRo
9+
itOFm+jgjWyWxGpeeINJAvWnrZoctGa2E6XFZGYwZABwd4Of46RDKAX0Juo1ZzaL
10+
QUClVMtnCuaoz99RW7PA+kqCVygphJ0BAdDkYIe5XwIDAQABo1MwUTAdBgNVHQ4E
11+
FgQU+hmH5wIOI3pavaSlU7Knzt7TRl8wHwYDVR0jBBgwFoAU+hmH5wIOI3pavaSl
12+
U7Knzt7TRl8wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEApPtQ
13+
ZF3wEIzJDyppcPCrwPxtYu26Ei6cLHtCa+YBg7boIFn1yi1wfcWt1iFDsbgpcSgt
14+
wF14UU1eOC1R8U+7jsLMHP7287CdEMmvLUEBkFIVSE0tl3YTBSwwNFJWBG98XBSQ
15+
GwlJtzgx3tJp66TjJ8IzK2W1Zq0lNOqHjqpVA99aQQnITttN2GZxNbXn8mn/KvZM
16+
4XB7EmfTJn9L8ByuVRhLPfnncRv6oy3Ac3MuC2tO/O6e5NspLCp7qoUv7D4G5Gpg
17+
USUmHFgy91hxbiB4t6QwJoDnaGF+5/tRVA4uW32SRrSvc/3+D7y0LyeJjmwNBsq8
18+
817XfO0PTLWS8OX2BA==
19+
-----END CERTIFICATE-----

0 commit comments

Comments
 (0)