> headers = HttpUtil.getWithOnlyResponseHeaders(resourceUri);
if (headers == null) {
throw new IllegalStateException("Could not obtain login URI to retrieve access token from.");
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java
index af7dd126a6a4..17c3688a5765 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java
@@ -55,10 +55,12 @@ final class AiaCertificateChainUtil {
private static final int AIA_CACHE_MAX_SIZE = 128;
private static final long MAX_SUCCESS_TTL_IN_MILLIS = TimeUnit.HOURS.toMillis(24);
private static final long NEGATIVE_TTL_IN_MILLIS = TimeUnit.MINUTES.toMillis(1);
- private static final AiaResponseCache AIA_CACHE
- = new AiaResponseCache(AIA_CACHE_MAX_SIZE, System::currentTimeMillis, (message, parameters) -> LOGGER.logp(FINE,
+ private static final AiaResponseCache AIA_CACHE =
+ new AiaResponseCache(AIA_CACHE_MAX_SIZE, System::currentTimeMillis, (message, parameters) -> LOGGER.logp(FINE,
AiaResponseCache.class.getName(), "diagnostic", message, parameters));
- private static final AiaResponseLoader DEFAULT_RESPONSE_LOADER = HttpUtil::getBytesWithMetadata;
+ // A default HTTP-based response loader for AIA requests.
+ private static final AiaResponseLoader DEFAULT_RESPONSE_LOADER = HttpUtil::getAiaBytesWithMetadata;
+ // The currently configured response loader, which can be overridden for tests.
private static volatile AiaResponseLoader responseLoader = DEFAULT_RESPONSE_LOADER;
/**
@@ -597,14 +599,29 @@ static void clearAiaCache() {
AIA_CACHE.clear();
}
+ /**
+ * Sets the response loader for AIA requests.
+ *
+ * This can be used to override the default HTTP-based loader, for example in tests.
+ *
+ * @param loader the response loader to use
+ */
static synchronized void setResponseLoader(AiaResponseLoader loader) {
responseLoader = Objects.requireNonNull(loader, "'loader' cannot be null.");
}
+ /**
+ * Resets the response loader for AIA requests to the default HTTP-based loader.
+ *
+ *
This can be used to undo any overrides set by {@link #setResponseLoader(AiaResponseLoader)}.
+ */
static synchronized void resetResponseLoader() {
responseLoader = DEFAULT_RESPONSE_LOADER;
}
+ /**
+ * Functional interface for loading AIA responses.
+ */
@FunctionalInterface
interface AiaResponseLoader {
HttpUtil.BinaryHttpResponse load(String url);
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/HttpUtil.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/HttpUtil.java
index 9188dd90686a..4014fe07cec7 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/HttpUtil.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/HttpUtil.java
@@ -50,30 +50,51 @@ public final class HttpUtil {
static final String DEFAULT_USER_AGENT_VALUE_PREFIX = "az-se-kv-jca/";
private static final Logger LOGGER = Logger.getLogger(HttpUtil.class.getName());
+
private static final int AIA_HTTP_TIMEOUT_IN_MILLISECONDS = 10_000;
static final int MAX_AIA_RESPONSE_SIZE_IN_BYTES = 10 * 1024 * 1024;
private static final int AIA_HTTP_TOTAL_TIMEOUT_IN_MILLISECONDS = 30_000;
private static final int MAX_AIA_REDIRECTS = 5;
+ @FunctionalInterface
+ interface ConnectionFactory {
+ HttpURLConnection open(String url) throws IOException;
+ }
+
+ /**
+ * Performs an HTTP GET request to the specified URI with the given headers.
+ *
+ * @param uri the URI to send the GET request to
+ * @param headers the headers to include in the request
+ * @return the response body as a string, or {@code null} if the request fails
+ */
public static String get(String uri, Map headers) {
return get(uri, headers, HttpUtil::openConnection);
}
+ // Overloaded method that allows specifying a custom ConnectionFactory for testing purposes.
static String get(String uri, Map headers, ConnectionFactory connectionFactory) {
HttpURLConnection connection = null;
+
try {
connection = connectionFactory.open(uri);
+
connection.setRequestMethod("GET");
if (headers != null) {
headers.forEach(connection::setRequestProperty);
}
+
connection.setRequestProperty(USER_AGENT_KEY, USER_AGENT_VALUE);
- ensureSuccessfulResponse(connection.getResponseCode());
- return readResponseBody(connection);
+ if (isSuccessfulResponse(connection.getResponseCode())) {
+ return readResponseBody(connection);
+ }
+
+ return null;
} catch (IOException ioe) {
LOGGER.log(WARNING, "Unable to finish the HTTP GET request.", ioe);
+
return null;
} finally {
if (connection != null) {
@@ -83,33 +104,92 @@ static String get(String uri, Map headers, ConnectionFactory con
}
/**
- * Performs an HTTP GET request and returns the raw response body as a byte array.
- * Used primarily for downloading DER-encoded certificates from CA Issuers URLs in
- * AIA (Authority Information Access) certificate extensions.
+ * Performs an HTTP POST request to the specified URI with the given headers, body, and content type.
*
- * @param url the URL to fetch
- * @return the response body bytes, or {@code null} if the request fails or returns non-2xx
+ * @param uri the URI to send the POST request to
+ * @param headers the headers to include in the request
+ * @param body the body of the POST request
+ * @param contentType the content type of the POST request body
+ * @return the response body as a string, or {@code null} if the request fails
*/
- public static byte[] getBytes(String url) {
- return getBytesWithMetadata(url).getBody();
+ public static String post(String uri, Map headers, String body, String contentType) {
+ return post(uri, headers, body, contentType, HttpUtil::openConnection);
}
- static BinaryHttpResponse getBytesWithMetadata(String url) {
- return getBytesWithMetadata(url, HttpUtil::openConnection);
+ // Overloaded method that allows specifying a custom ConnectionFactory for testing purposes.
+ static String post(String uri, Map headers, String body, String contentType,
+ ConnectionFactory connectionFactory) {
+
+ HttpURLConnection connection = null;
+
+ try {
+ connection = connectionFactory.open(uri);
+
+ connection.setRequestMethod("POST");
+ connection.setDoOutput(true);
+
+ if (headers != null) {
+ headers.forEach(connection::setRequestProperty);
+ }
+
+ if (contentType != null) {
+ connection.setRequestProperty("Content-Type", contentType);
+ }
+
+ connection.setRequestProperty(USER_AGENT_KEY, USER_AGENT_VALUE);
+
+ try (OutputStream outputStream = connection.getOutputStream()) {
+ outputStream.write(body.getBytes(StandardCharsets.UTF_8));
+ }
+
+ if (isSuccessfulResponse(connection.getResponseCode())) {
+ return readResponseBody(connection);
+ }
+
+ return null;
+ } catch (IOException ioe) {
+ LOGGER.log(WARNING, "Unable to finish the HTTP POST request.", ioe);
+
+ return null;
+ } finally {
+ if (connection != null) {
+ connection.disconnect();
+ }
+ }
+ }
+
+ /**
+ * Performs an HTTP GET request and returns the response body along with HTTP metadata. Used primarily for
+ * downloading DER-encoded certificates from CA Issuers URLs in AIA (Authority Information Access) certificate
+ * extensions.
+ *
+ * @param url the URL to fetch
+ * @return the response body bytes, or {@code null} if the request fails or returns non-2xx
+ */
+ public static BinaryHttpResponse getAiaBytesWithMetadata(String url) {
+ return getAiaBytesWithMetadata(url, HttpUtil::openConnection);
}
- static BinaryHttpResponse getBytesWithMetadata(String url, ConnectionFactory connectionFactory) {
+ // Overloaded method that allows specifying a custom ConnectionFactory for testing purposes.
+ static BinaryHttpResponse getAiaBytesWithMetadata(String url, ConnectionFactory connectionFactory) {
String currentUrl;
+
try {
currentUrl = validateAiaUrl(url);
} catch (IllegalArgumentException e) {
LOGGER.log(WARNING, "Unable to finish the HTTP GET (bytes) request for URL: " + url, e);
+
return BinaryHttpResponse.empty();
}
+
+ // HttpURLConnection does not follow redirects between different protocols (e.g., HTTP to HTTPS)
+ // reliably, so we handle redirects ourselves. AIA uses a small number of redirects, if any.
for (int redirectCount = 0; redirectCount <= MAX_AIA_REDIRECTS; redirectCount++) {
HttpURLConnection connection = null;
+
try {
- connection = connectionFactory.open(currentUrl);
+ connection = connectionFactory.open(currentUrl);
+
connection.setInstanceFollowRedirects(false);
connection.setRequestMethod("GET");
connection.setConnectTimeout(AIA_HTTP_TIMEOUT_IN_MILLISECONDS);
@@ -121,31 +201,44 @@ static BinaryHttpResponse getBytesWithMetadata(String url, ConnectionFactory con
String date = connection.getHeaderField("Date");
String age = connection.getHeaderField("Age");
String expires = connection.getHeaderField("Expires");
+
if (isRedirect(status)) {
String location = connection.getHeaderField("Location");
+
if (location == null || redirectCount == MAX_AIA_REDIRECTS) {
LOGGER.log(WARNING, "HTTP GET redirect could not be followed for URL: {0}", currentUrl);
+
return new BinaryHttpResponse(null, cacheControl, date, age, expires);
}
+
currentUrl = resolveAiaRedirect(currentUrl, location);
+
continue;
}
+
if (status < 200 || status >= 300) {
LOGGER.log(WARNING, "HTTP GET returned status {0} for URL: {1}",
new Object[] { status, currentUrl });
+
return new BinaryHttpResponse(null, cacheControl, date, age, expires);
}
long contentLength = connection.getContentLengthLong();
+
if (contentLength > MAX_AIA_RESPONSE_SIZE_IN_BYTES) {
LOGGER.log(WARNING, "AIA response exceeded the maximum size for URL: {0}", currentUrl);
+
return new BinaryHttpResponse(null, cacheControl, date, age, expires);
}
return new BinaryHttpResponse(readResponseBytes(connection.getInputStream(), currentUrl), cacheControl,
date, age, expires);
} catch (IOException | IllegalArgumentException | ClassCastException | UncheckedIOException e) {
+ // Catch all exceptions including IOException, IllegalArgumentException, and other runtime exceptions
+ // that may occur during HTTP execution. Gracefully return null to allow AIA completion to fail silently
+ // the entire jarsigner/signing operation.
LOGGER.log(WARNING, "Unable to finish the HTTP GET (bytes) request for URL: " + currentUrl, e);
+
return BinaryHttpResponse.empty();
} finally {
if (connection != null) {
@@ -153,6 +246,7 @@ static BinaryHttpResponse getBytesWithMetadata(String url, ConnectionFactory con
}
}
}
+
return BinaryHttpResponse.empty();
}
@@ -168,66 +262,85 @@ private static String resolveAiaRedirect(String currentUrl, String location) {
if (location.startsWith("?")) {
int queryIndex = currentUrl.indexOf('?');
int fragmentIndex = currentUrl.indexOf('#');
- int suffixIndex
- = queryIndex < 0 ? fragmentIndex : fragmentIndex < 0 ? queryIndex : Math.min(queryIndex, fragmentIndex);
+ int suffixIndex =
+ queryIndex < 0 ? fragmentIndex : fragmentIndex < 0 ? queryIndex : Math.min(queryIndex, fragmentIndex);
String currentUrlWithoutSuffix = suffixIndex < 0 ? currentUrl : currentUrl.substring(0, suffixIndex);
+
return validateAiaUrl(currentUrlWithoutSuffix + location);
}
+
return validateAiaUrl(URI.create(currentUrl).resolve(location).toString());
}
private static String validateAiaUrl(String url) {
URI uri = URI.create(url);
String scheme = uri.getScheme();
+
if (!"http".equalsIgnoreCase(scheme) && !"https".equalsIgnoreCase(scheme)) {
throw new IllegalArgumentException("AIA URL must use HTTP or HTTPS.");
}
+
return uri.toString();
}
private static byte[] readResponseBytes(InputStream inputStream, String url) throws IOException {
long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(AIA_HTTP_TOTAL_TIMEOUT_IN_MILLISECONDS);
+
try (InputStream responseBody = inputStream; ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int totalBytesRead = 0;
int read;
+
while ((read = responseBody.read(buffer)) != -1) {
if (read > MAX_AIA_RESPONSE_SIZE_IN_BYTES - totalBytesRead) {
LOGGER.log(WARNING, "AIA response exceeded the maximum size for URL: {0}", url);
+
return null;
}
+
outputStream.write(buffer, 0, read);
+
totalBytesRead += read;
+
if (System.nanoTime() > deadline) {
LOGGER.log(WARNING, "AIA response exceeded the maximum download time for URL: {0}", url);
+
return null;
}
}
+
return outputStream.toByteArray();
}
}
private static String getCombinedHeaderValue(HttpURLConnection connection, String name) {
Map> headers = connection.getHeaderFields();
+
if (headers == null) {
- return connection.getHeaderField(name);
+ return null;
}
+
StringBuilder value = new StringBuilder();
+
for (Map.Entry> entry : headers.entrySet()) {
if (entry.getKey() == null || !name.equalsIgnoreCase(entry.getKey()) || entry.getValue() == null) {
continue;
}
+
for (String headerValue : entry.getValue()) {
if (headerValue == null) {
continue;
}
+
if (value.length() > 0) {
value.append(", ");
}
+
value.append(headerValue);
}
}
- return value.length() == 0 ? connection.getHeaderField(name) : value.toString();
+
+ return value.length() == 0 ? null : value.toString();
}
static final class BinaryHttpResponse {
@@ -269,16 +382,7 @@ String getExpires() {
return expires;
}
}
-
- public static String post(String uri, String body, String contentType) {
- return post(uri, null, body, contentType);
- }
-
- @FunctionalInterface
- interface ConnectionFactory {
- HttpURLConnection open(String url) throws IOException;
- }
-
+
public static String getUserAgentPrefix() {
return Optional.of(HttpUtil.class)
.map(Class::getClassLoader)
@@ -291,86 +395,66 @@ public static String getUserAgentPrefix() {
.orElse(DEFAULT_USER_AGENT_VALUE_PREFIX);
}
- public static String post(String uri, Map headers, String body, String contentType) {
- return post(uri, headers, body, contentType, HttpUtil::openConnection);
- }
-
- static String post(String uri, Map headers, String body, String contentType,
- ConnectionFactory connectionFactory) {
- HttpURLConnection connection = null;
- try {
- connection = connectionFactory.open(uri);
- connection.setRequestMethod("POST");
- connection.setDoOutput(true);
-
- if (headers != null) {
- headers.forEach(connection::setRequestProperty);
- }
- if (contentType != null) {
- connection.setRequestProperty("Content-Type", contentType);
- }
- connection.setRequestProperty(USER_AGENT_KEY, USER_AGENT_VALUE);
- try (OutputStream outputStream = connection.getOutputStream()) {
- outputStream.write(body.getBytes(StandardCharsets.UTF_8));
- }
-
- ensureSuccessfulResponse(connection.getResponseCode());
- return readResponseBody(connection);
- } catch (IOException ioe) {
- LOGGER.log(WARNING, "Unable to finish the HTTP POST request.", ioe);
- return null;
- } finally {
- if (connection != null) {
- connection.disconnect();
- }
- }
- }
-
private static String createErrorMessage(int status) {
- return "Fail to get response from Key Vault because return http status code is " + status + ". It can be "
+ return "Failed to get response from Key Vault because return http status code is " + status + ". It can be "
+ "caused by missing permissions or roles. To know how to add permissions or roles, see "
+ "https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/keyvault/azure-security-keyvault-jca#prerequisites.";
}
- private static void ensureSuccessfulResponse(int status) {
- if (status < 200 || status >= 300) {
- String errorMessage = createErrorMessage(status);
- LOGGER.log(SEVERE, errorMessage);
- throw new RuntimeException(errorMessage);
+ private static boolean isSuccessfulResponse(int status) {
+ if (status >= 200 && status < 300) {
+ return true;
}
+
+ LOGGER.log(SEVERE, createErrorMessage(status));
+
+ return false;
}
@SuppressWarnings("StringOperationCanBeSimplified")
private static String readResponseBody(HttpURLConnection connection) throws IOException {
try (InputStream responseBody = connection.getInputStream();
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
+
if (responseBody == null) {
+
return null;
}
+
byte[] buffer = new byte[4096];
int read;
+
while ((read = responseBody.read(buffer)) != -1) {
outputStream.write(buffer, 0, read);
}
+
return new String(outputStream.toByteArray(), StandardCharsets.UTF_8);
}
}
- public static Map> getWithResponseHeadersOnlyReturn(String uri) {
- return getWithResponseHeadersOnlyReturn(uri, HttpUtil::openConnection);
+ /**
+ * Retrieves only the response headers from an HTTP GET request to the specified URI.
+ *
+ * @param uri the URI to send the HTTP GET request to
+ * @return a map of response headers, or null if the request was not successful
+ */
+ public static Map> getWithOnlyResponseHeaders(String uri) {
+ return getWithOnlyResponseHeaders(uri, HttpUtil::openConnection);
}
- static Map> getWithResponseHeadersOnlyReturn(String uri, ConnectionFactory connectionFactory) {
+ static Map> getWithOnlyResponseHeaders(String uri, ConnectionFactory connectionFactory) {
HttpURLConnection connection = null;
+
try {
connection = connectionFactory.open(uri);
- connection.setRequestMethod("GET");
+ connection.setRequestMethod("GET");
connection.setRequestProperty(USER_AGENT_KEY, USER_AGENT_VALUE);
if (connection.getResponseCode() == 401) {
Map> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
Map> responseHeaders = connection.getHeaderFields();
+
if (responseHeaders != null) {
responseHeaders.forEach((name, values) -> {
if (name != null) {
@@ -378,11 +462,14 @@ static Map> getWithResponseHeadersOnlyReturn(String uri, Co
}
});
}
+
return headers;
}
+
return null;
} catch (IOException ioe) {
LOGGER.log(WARNING, "Unable to finish the HTTP GET request.", ioe);
+
return null;
} finally {
if (connection != null) {
@@ -394,12 +481,16 @@ static Map> getWithResponseHeadersOnlyReturn(String uri, Co
private static HttpURLConnection openConnection(String uri) {
try {
HttpURLConnection connection = (HttpURLConnection) URI.create(uri).toURL().openConnection();
+
if (connection instanceof HttpsURLConnection) {
try {
- TrustManagerFactory trustManagerFactory
- = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ TrustManagerFactory trustManagerFactory =
+ TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+
trustManagerFactory.init(JreKeyStoreFactory.getDefaultKeyStore());
+
SSLContext sslContext = SSLContext.getInstance("TLS");
+
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
((HttpsURLConnection) connection).setSSLSocketFactory(sslContext.getSocketFactory());
} catch (KeyManagementException | KeyStoreException | NoSuchAlgorithmException e) {
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificatesTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificatesTest.java
index 8ccfc915c5b7..6183339d122b 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificatesTest.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificatesTest.java
@@ -1260,14 +1260,17 @@ private synchronized void sequence(K key, List> suppliers) {
private void returnValues(K key, List values) {
List> suppliers = new ArrayList<>();
+
for (V value : values) {
suppliers.add(() -> value);
}
+
sequence(key, suppliers);
}
private void throwThenReturn(K key, RuntimeException exception, V value) {
List> suppliers = new ArrayList<>();
+
suppliers.add(() -> {
throw exception;
});
@@ -1281,6 +1284,7 @@ private void answer(K key, Supplier supplier) {
private void answerThenReturn(K key, Supplier supplier, V value) {
List> suppliers = new ArrayList<>();
+
suppliers.add(supplier);
suppliers.add(() -> value);
sequence(key, suppliers);
@@ -1288,23 +1292,31 @@ private void answerThenReturn(K key, Supplier supplier, V value) {
private int callCount(K key) {
AtomicInteger counter;
+
synchronized (this) {
counter = callCounts.get(key);
}
+
return counter == null ? 0 : counter.get();
}
private V invoke(K key) {
Supplier supplier;
+
synchronized (this) {
AtomicInteger counter = callCounts.computeIfAbsent(key, unused -> new AtomicInteger());
+
counter.incrementAndGet();
+
Deque> suppliers = queuedSuppliers.get(key);
+
if (suppliers == null || suppliers.isEmpty()) {
return null;
}
+
supplier = suppliers.size() > 1 ? suppliers.poll() : suppliers.peek();
}
+
// The actual supplier invocation happens outside the synchronized block so blocking suppliers used by
// concurrency tests don't serialize unrelated calls against this same script.
return supplier.get();
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/HttpUtilTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/HttpUtilTest.java
index a37dafcde0e1..ff3a2b03abf7 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/HttpUtilTest.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/HttpUtilTest.java
@@ -58,8 +58,8 @@ public void testHttpUtilGet1() {
@Test
void textGetReturnsSuccessfulResponseBody() throws Exception {
byte[] body = "response".getBytes(StandardCharsets.UTF_8);
- TestHttpURLConnection connection
- = new TestHttpURLConnection("https://example.test/value", 200, body, Collections.emptyMap());
+ TestHttpURLConnection connection =
+ new TestHttpURLConnection("https://example.test/value", 200, body, Collections.emptyMap());
String result = HttpUtil.get("https://example.test/value", Collections.singletonMap("x-test", "value"),
ignored -> connection);
@@ -83,34 +83,34 @@ void textGetThrowsForNonSuccessfulResponse() throws Exception {
}
@Test
- void postThrowsForNonSuccessfulResponse() throws Exception {
+ void postReturnsNullForNonSuccessfulResponse() throws Exception {
TestHttpURLConnection connection = new TestHttpURLConnection("https://example.test/value", 403,
"{\"error\":\"forbidden\"}".getBytes(StandardCharsets.UTF_8), Collections.emptyMap());
- RuntimeException exception = assertThrows(RuntimeException.class, () -> HttpUtil
- .post("https://example.test/value", null, "request", "application/json", ignored -> connection));
+ String result = HttpUtil
+ .post("https://example.test/value", null, "request", "application/json", ignored -> connection);
- assertTrue(exception.getMessage().contains("403"));
+ assertNull(result);
assertEquals("request", new String(connection.requestBody.toByteArray(), StandardCharsets.UTF_8));
assertTrue(connection.disconnected);
}
@Test
void authenticationChallengeReturnsCaseInsensitiveHeadersOnlyFor401() throws Exception {
- Map> headers
- = Collections.singletonMap("www-authenticate", Collections.singletonList("Bearer authorization=test"));
- TestHttpURLConnection unauthorized
- = new TestHttpURLConnection("https://example.test/challenge", 401, new byte[0], headers);
+ Map> headers =
+ Collections.singletonMap("www-authenticate", Collections.singletonList("Bearer authorization=test"));
+ TestHttpURLConnection unauthorized =
+ new TestHttpURLConnection("https://example.test/challenge", 401, new byte[0], headers);
- Map> result
- = HttpUtil.getWithResponseHeadersOnlyReturn("https://example.test/challenge", ignored -> unauthorized);
+ Map> result =
+ HttpUtil.getWithOnlyResponseHeaders("https://example.test/challenge", ignored -> unauthorized);
assertEquals("Bearer authorization=test", result.get("WWW-Authenticate").get(0));
assertTrue(unauthorized.disconnected);
- TestHttpURLConnection successful
- = new TestHttpURLConnection("https://example.test/challenge", 200, new byte[0], headers);
- assertNull(HttpUtil.getWithResponseHeadersOnlyReturn("https://example.test/challenge", ignored -> successful));
+ TestHttpURLConnection successful =
+ new TestHttpURLConnection("https://example.test/challenge", 200, new byte[0], headers);
+ assertNull(HttpUtil.getWithOnlyResponseHeaders("https://example.test/challenge", ignored -> successful));
assertTrue(successful.disconnected);
}
@@ -124,8 +124,8 @@ void binaryResponsePreservesBodyAndFreshnessHeaders() throws Exception {
headers.put("Expires", Collections.singletonList("Wed, 05 Aug 2026 10:05:00 GMT"));
TestHttpURLConnection connection = new TestHttpURLConnection(200, body, headers);
- HttpUtil.BinaryHttpResponse result
- = HttpUtil.getBytesWithMetadata("https://example.test/cert.crt", ignored -> connection);
+ HttpUtil.BinaryHttpResponse result =
+ HttpUtil.getAiaBytesWithMetadata("https://example.test/cert.crt", ignored -> connection);
assertArrayEquals(body, result.getBody());
assertEquals("public, max-age=300", result.getCacheControl());
@@ -140,12 +140,12 @@ void binaryResponsePreservesBodyAndFreshnessHeaders() throws Exception {
@Test
void binaryResponseForFailureHasNoBodyAndPreservesFreshnessMetadata() throws Exception {
- Map> headers
- = Collections.singletonMap("Cache-Control", Collections.singletonList("max-age=3600"));
+ Map> headers =
+ Collections.singletonMap("Cache-Control", Collections.singletonList("max-age=3600"));
TestHttpURLConnection connection = new TestHttpURLConnection(503, new byte[] { 1 }, headers);
- HttpUtil.BinaryHttpResponse result
- = HttpUtil.getBytesWithMetadata("https://example.test/cert.crt", ignored -> connection);
+ HttpUtil.BinaryHttpResponse result =
+ HttpUtil.getAiaBytesWithMetadata("https://example.test/cert.crt", ignored -> connection);
assertNull(result.getBody());
assertEquals("max-age=3600", result.getCacheControl());
@@ -160,8 +160,8 @@ void binaryResponseCombinesMultipleCacheControlHeaders() throws Exception {
headers.put("cache-control", Arrays.asList("public, max-age=300", "no-store"));
TestHttpURLConnection connection = new TestHttpURLConnection(200, new byte[] { 1 }, headers);
- HttpUtil.BinaryHttpResponse result
- = HttpUtil.getBytesWithMetadata("https://example.test/cert.crt", ignored -> connection);
+ HttpUtil.BinaryHttpResponse result =
+ HttpUtil.getAiaBytesWithMetadata("https://example.test/cert.crt", ignored -> connection);
assertEquals("public, max-age=300, no-store", result.getCacheControl());
}
@@ -172,8 +172,8 @@ void binaryResponseRejectsOversizedContentLength() throws Exception {
Collections.singletonList(String.valueOf(HttpUtil.MAX_AIA_RESPONSE_SIZE_IN_BYTES + 1)));
TestHttpURLConnection connection = new TestHttpURLConnection(200, new byte[] { 1 }, headers);
- HttpUtil.BinaryHttpResponse result
- = HttpUtil.getBytesWithMetadata("https://example.test/cert.crt", ignored -> connection);
+ HttpUtil.BinaryHttpResponse result =
+ HttpUtil.getAiaBytesWithMetadata("https://example.test/cert.crt", ignored -> connection);
assertNull(result.getBody());
assertTrue(connection.disconnected);
@@ -184,8 +184,8 @@ void binaryResponseRejectsStreamThatExceedsMaximumSize() throws Exception {
byte[] body = new byte[HttpUtil.MAX_AIA_RESPONSE_SIZE_IN_BYTES + 1];
TestHttpURLConnection connection = new TestHttpURLConnection(200, body, Collections.emptyMap());
- HttpUtil.BinaryHttpResponse result
- = HttpUtil.getBytesWithMetadata("https://example.test/cert.crt", ignored -> connection);
+ HttpUtil.BinaryHttpResponse result =
+ HttpUtil.getAiaBytesWithMetadata("https://example.test/cert.crt", ignored -> connection);
assertNull(result.getBody());
assertTrue(connection.disconnected);
@@ -195,13 +195,13 @@ void binaryResponseRejectsStreamThatExceedsMaximumSize() throws Exception {
void binaryResponseFollowsHttpToHttpsRedirect() throws Exception {
String sourceUrl = "http://example.test/cert.crt";
String targetUrl = "https://example.test/cert.crt";
- Map> redirectHeaders
- = Collections.singletonMap("Location", Collections.singletonList(targetUrl));
+ Map> redirectHeaders =
+ Collections.singletonMap("Location", Collections.singletonList(targetUrl));
TestHttpURLConnection redirect = new TestHttpURLConnection(sourceUrl, 302, new byte[0], redirectHeaders);
byte[] body = new byte[] { 1, 2, 3 };
TestHttpURLConnection response = new TestHttpURLConnection(targetUrl, 200, body, Collections.emptyMap());
- HttpUtil.BinaryHttpResponse result = HttpUtil.getBytesWithMetadata(sourceUrl, url -> {
+ HttpUtil.BinaryHttpResponse result = HttpUtil.getAiaBytesWithMetadata(sourceUrl, url -> {
if (sourceUrl.equals(url)) {
return redirect;
}
@@ -221,13 +221,13 @@ void binaryResponseFollowsHttpToHttpsRedirect() throws Exception {
void binaryResponsePreservesPathForQueryOnlyRedirect() throws Exception {
String sourceUrl = "https://example.test/certificates/issuer.crt?v=1";
String targetUrl = "https://example.test/certificates/issuer.crt?v=2";
- Map> redirectHeaders
- = Collections.singletonMap("Location", Collections.singletonList("?v=2"));
+ Map> redirectHeaders =
+ Collections.singletonMap("Location", Collections.singletonList("?v=2"));
TestHttpURLConnection redirect = new TestHttpURLConnection(sourceUrl, 302, new byte[0], redirectHeaders);
byte[] body = new byte[] { 1, 2, 3 };
TestHttpURLConnection response = new TestHttpURLConnection(targetUrl, 200, body, Collections.emptyMap());
- HttpUtil.BinaryHttpResponse result = HttpUtil.getBytesWithMetadata(sourceUrl, url -> {
+ HttpUtil.BinaryHttpResponse result = HttpUtil.getAiaBytesWithMetadata(sourceUrl, url -> {
if (sourceUrl.equals(url)) {
return redirect;
}
From 60ace7adc962893a68da301a124728fe44f91e3a Mon Sep 17 00:00:00 2001
From: Victor Colin Amador
Date: Thu, 27 Aug 2026 02:42:35 -0700
Subject: [PATCH 10/16] Reverted some changes
---
.../stress/scenarios/EventForwarder.java | 3 +-
.../stress/util/TelemetryHelper.java | 2 +-
.../jca/implementation/KeyVaultClient.java | 34 ++++++---
.../utils/AiaCertificateChainUtil.java | 4 +-
.../jca/implementation/utils/HttpUtil.java | 76 ++++++++-----------
.../KeyVaultCertificatesTest.java | 14 +---
.../implementation/utils/HttpUtilTest.java | 72 ++++++++++--------
7 files changed, 103 insertions(+), 102 deletions(-)
diff --git a/sdk/eventhubs/azure-messaging-eventhubs-stress/src/main/java/com/azure/messaging/eventhubs/stress/scenarios/EventForwarder.java b/sdk/eventhubs/azure-messaging-eventhubs-stress/src/main/java/com/azure/messaging/eventhubs/stress/scenarios/EventForwarder.java
index 4fd67886541d..907ecf0f6873 100644
--- a/sdk/eventhubs/azure-messaging-eventhubs-stress/src/main/java/com/azure/messaging/eventhubs/stress/scenarios/EventForwarder.java
+++ b/sdk/eventhubs/azure-messaging-eventhubs-stress/src/main/java/com/azure/messaging/eventhubs/stress/scenarios/EventForwarder.java
@@ -105,8 +105,7 @@ public void run() {
private EventHubProducerAsyncClient getForwardProducer() {
final TokenCredential tokenCredential = new DefaultAzureCredentialBuilder().build();
final EventHubClientBuilder builder = new EventHubClientBuilder()
- .credential(options.getEventHubsFullyQualifiedNamespace(), forwardEventHubName,
- tokenCredential)
+ .credential(options.getEventHubsFullyQualifiedNamespace(), forwardEventHubName, tokenCredential)
.retryOptions(new AmqpRetryOptions().setTryTimeout(Duration.ofSeconds(10)))
.transportType(options.getAmqpTransportType())
.consumerGroup(options.getEventHubsConsumerGroup());
diff --git a/sdk/eventhubs/azure-messaging-eventhubs-stress/src/main/java/com/azure/messaging/eventhubs/stress/util/TelemetryHelper.java b/sdk/eventhubs/azure-messaging-eventhubs-stress/src/main/java/com/azure/messaging/eventhubs/stress/util/TelemetryHelper.java
index 393155d8914e..0f9f6a37a768 100644
--- a/sdk/eventhubs/azure-messaging-eventhubs-stress/src/main/java/com/azure/messaging/eventhubs/stress/util/TelemetryHelper.java
+++ b/sdk/eventhubs/azure-messaging-eventhubs-stress/src/main/java/com/azure/messaging/eventhubs/stress/util/TelemetryHelper.java
@@ -90,7 +90,7 @@ public TelemetryHelper(Class> scenarioClass) {
*/
private static OpenTelemetry init() {
System.setProperty("otel.java.global-autoconfigure.enabled", "true");
-
+
AutoConfiguredOpenTelemetrySdkBuilder sdkBuilder = AutoConfiguredOpenTelemetrySdk.builder();
String applicationInsightsConnectionString = System.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING");
if (applicationInsightsConnectionString == null) {
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java
index 86117827d975..aa10798b7d90 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/KeyVaultClient.java
@@ -257,14 +257,13 @@ private AccessToken obtainAccessToken() {
LOGGER.info("Using client credentials (client ID/secret) for authentication");
String aadAuthenticationUri = getLoginUri(keyVaultUri + "certificates" + API_VERSION_POSTFIX,
disableChallengeResourceVerification);
- result
- = AccessTokenUtil.getAccessToken(resource, aadAuthenticationUri, tenantId, clientId, clientSecret);
+ result = getAccessToken(resource, aadAuthenticationUri, tenantId, clientId, clientSecret);
} else if (AccessTokenUtil.isWorkloadIdentityAvailable(clientId, tenantId)) {
LOGGER.info("Using workload identity for authentication");
result = AccessTokenUtil.getAccessTokenWithWorkloadIdentity(keyVaultBaseUri, tenantId, clientId);
} else if (managedIdentity != null) {
LOGGER.info("Using managed identity for authentication");
- result = AccessTokenUtil.getAccessToken(resource, managedIdentity);
+ result = getAccessToken(resource, managedIdentity);
} else if (providedAccessToken != null && !providedAccessToken.isEmpty()) {
LOGGER.info("Using provided access token for authentication");
// Create an AccessToken object from the provided token string
@@ -274,7 +273,7 @@ private AccessToken obtainAccessToken() {
result = new AccessToken(providedAccessToken, Long.MAX_VALUE / 1000);
} else {
LOGGER.info("Using managed identity for authentication (default)");
- result = AccessTokenUtil.getAccessToken(resource, null);
+ result = getAccessToken(resource, null);
}
} catch (UnsupportedEncodingException e) {
LOGGER.log(WARNING, "Could not obtain access token to authenticate with.", e);
@@ -298,7 +297,7 @@ public List getAliases() {
String uri = keyVaultUri + "certificates" + API_VERSION_POSTFIX;
while (uri != null && !uri.isEmpty()) {
- String response = HttpUtil.get(uri, headers);
+ String response = httpGet(uri, headers);
CertificateListResult certificateListResult = null;
if (response != null) {
@@ -344,7 +343,7 @@ private CertificateBundle getCertificateBundle(String alias) {
LOGGER.entering("KeyVaultClient", "getCertificateBundle", alias);
CertificateBundle result = null;
- String response = HttpUtil.get(keyVaultUri + "certificates/" + alias + API_VERSION_POSTFIX,
+ String response = httpGet(keyVaultUri + "certificates/" + alias + API_VERSION_POSTFIX,
Collections.singletonMap("Authorization", "Bearer " + getAccessToken()));
if (response != null) {
@@ -460,7 +459,7 @@ public Certificate[] getCertificateChainForVersion(CertificateVersion certificat
return new Certificate[0];
}
- String response = HttpUtil.get(certificateVersion.getSecretId() + API_VERSION_POSTFIX,
+ String response = httpGet(certificateVersion.getSecretId() + API_VERSION_POSTFIX,
Collections.singletonMap("Authorization", "Bearer " + getAccessToken()));
if (response == null) {
@@ -541,7 +540,7 @@ public Key getKeyForVersion(CertificateVersion certificateVersion, char[] passwo
return null;
}
- String body = HttpUtil.get(certificateSecretUri + API_VERSION_POSTFIX,
+ String body = httpGet(certificateSecretUri + API_VERSION_POSTFIX,
Collections.singletonMap("Authorization", "Bearer " + getAccessToken()));
if (body == null) {
@@ -614,7 +613,7 @@ public byte[] getSignedWithPrivateKey(String digestName, String digestValue, Str
String bodyString = "{\"alg\": \"" + digestName + "\", \"value\": \"" + digestValue + "\"}";
Map headers = Collections.singletonMap("Authorization", "Bearer " + getAccessToken());
String uri = keyId + "/sign" + API_VERSION_POSTFIX;
- String response = HttpUtil.post(uri, headers, bodyString, "application/json");
+ String response = httpPost(uri, headers, bodyString);
if (response != null) {
try {
@@ -689,4 +688,21 @@ private PrivateKey createPrivateKeyFromPem(String pemString, String keyType)
return privateKey;
}
+
+ String httpGet(String uri, Map headers) {
+ return HttpUtil.get(uri, headers);
+ }
+
+ String httpPost(String uri, Map headers, String body) {
+ return HttpUtil.post(uri, headers, body, "application/json");
+ }
+
+ AccessToken getAccessToken(String resource, String identity) {
+ return AccessTokenUtil.getAccessToken(resource, identity);
+ }
+
+ AccessToken getAccessToken(String resource, String aadAuthenticationUri, String tenantId, String clientId,
+ String clientSecret) {
+ return AccessTokenUtil.getAccessToken(resource, aadAuthenticationUri, tenantId, clientId, clientSecret);
+ }
}
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java
index 17c3688a5765..c34f9319d97d 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java
@@ -55,8 +55,8 @@ final class AiaCertificateChainUtil {
private static final int AIA_CACHE_MAX_SIZE = 128;
private static final long MAX_SUCCESS_TTL_IN_MILLIS = TimeUnit.HOURS.toMillis(24);
private static final long NEGATIVE_TTL_IN_MILLIS = TimeUnit.MINUTES.toMillis(1);
- private static final AiaResponseCache AIA_CACHE =
- new AiaResponseCache(AIA_CACHE_MAX_SIZE, System::currentTimeMillis, (message, parameters) -> LOGGER.logp(FINE,
+ private static final AiaResponseCache AIA_CACHE
+ = new AiaResponseCache(AIA_CACHE_MAX_SIZE, System::currentTimeMillis, (message, parameters) -> LOGGER.logp(FINE,
AiaResponseCache.class.getName(), "diagnostic", message, parameters));
// A default HTTP-based response loader for AIA requests.
private static final AiaResponseLoader DEFAULT_RESPONSE_LOADER = HttpUtil::getAiaBytesWithMetadata;
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/HttpUtil.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/HttpUtil.java
index 4014fe07cec7..04d517502f0c 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/HttpUtil.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/HttpUtil.java
@@ -51,6 +51,7 @@ public final class HttpUtil {
private static final Logger LOGGER = Logger.getLogger(HttpUtil.class.getName());
+ static final int HTTP_TIMEOUT_IN_MILLISECONDS = 180_000;
private static final int AIA_HTTP_TIMEOUT_IN_MILLISECONDS = 10_000;
static final int MAX_AIA_RESPONSE_SIZE_IN_BYTES = 10 * 1024 * 1024;
private static final int AIA_HTTP_TOTAL_TIMEOUT_IN_MILLISECONDS = 30_000;
@@ -67,6 +68,7 @@ interface ConnectionFactory {
* @param uri the URI to send the GET request to
* @param headers the headers to include in the request
* @return the response body as a string, or {@code null} if the request fails
+ * @throws RuntimeException if the server returns a non-successful response
*/
public static String get(String uri, Map headers) {
return get(uri, headers, HttpUtil::openConnection);
@@ -78,20 +80,10 @@ static String get(String uri, Map headers, ConnectionFactory con
try {
connection = connectionFactory.open(uri);
+ configureConnection(connection, "GET", headers);
- connection.setRequestMethod("GET");
-
- if (headers != null) {
- headers.forEach(connection::setRequestProperty);
- }
-
- connection.setRequestProperty(USER_AGENT_KEY, USER_AGENT_VALUE);
-
- if (isSuccessfulResponse(connection.getResponseCode())) {
- return readResponseBody(connection);
- }
-
- return null;
+ ensureSuccessfulResponse(connection.getResponseCode());
+ return readResponseBody(connection);
} catch (IOException ioe) {
LOGGER.log(WARNING, "Unable to finish the HTTP GET request.", ioe);
@@ -111,6 +103,7 @@ static String get(String uri, Map headers, ConnectionFactory con
* @param body the body of the POST request
* @param contentType the content type of the POST request body
* @return the response body as a string, or {@code null} if the request fails
+ * @throws RuntimeException if the server returns a non-successful response
*/
public static String post(String uri, Map headers, String body, String contentType) {
return post(uri, headers, body, contentType, HttpUtil::openConnection);
@@ -124,29 +117,19 @@ static String post(String uri, Map headers, String body, String
try {
connection = connectionFactory.open(uri);
-
- connection.setRequestMethod("POST");
+ configureConnection(connection, "POST", headers);
connection.setDoOutput(true);
- if (headers != null) {
- headers.forEach(connection::setRequestProperty);
- }
-
if (contentType != null) {
connection.setRequestProperty("Content-Type", contentType);
}
- connection.setRequestProperty(USER_AGENT_KEY, USER_AGENT_VALUE);
-
try (OutputStream outputStream = connection.getOutputStream()) {
outputStream.write(body.getBytes(StandardCharsets.UTF_8));
}
- if (isSuccessfulResponse(connection.getResponseCode())) {
- return readResponseBody(connection);
- }
-
- return null;
+ ensureSuccessfulResponse(connection.getResponseCode());
+ return readResponseBody(connection);
} catch (IOException ioe) {
LOGGER.log(WARNING, "Unable to finish the HTTP POST request.", ioe);
@@ -188,7 +171,7 @@ static BinaryHttpResponse getAiaBytesWithMetadata(String url, ConnectionFactory
HttpURLConnection connection = null;
try {
- connection = connectionFactory.open(currentUrl);
+ connection = connectionFactory.open(currentUrl);
connection.setInstanceFollowRedirects(false);
connection.setRequestMethod("GET");
@@ -262,8 +245,8 @@ private static String resolveAiaRedirect(String currentUrl, String location) {
if (location.startsWith("?")) {
int queryIndex = currentUrl.indexOf('?');
int fragmentIndex = currentUrl.indexOf('#');
- int suffixIndex =
- queryIndex < 0 ? fragmentIndex : fragmentIndex < 0 ? queryIndex : Math.min(queryIndex, fragmentIndex);
+ int suffixIndex
+ = queryIndex < 0 ? fragmentIndex : fragmentIndex < 0 ? queryIndex : Math.min(queryIndex, fragmentIndex);
String currentUrlWithoutSuffix = suffixIndex < 0 ? currentUrl : currentUrl.substring(0, suffixIndex);
return validateAiaUrl(currentUrlWithoutSuffix + location);
@@ -382,7 +365,7 @@ String getExpires() {
return expires;
}
}
-
+
public static String getUserAgentPrefix() {
return Optional.of(HttpUtil.class)
.map(Class::getClassLoader)
@@ -401,20 +384,18 @@ private static String createErrorMessage(int status) {
+ "https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/keyvault/azure-security-keyvault-jca#prerequisites.";
}
- private static boolean isSuccessfulResponse(int status) {
- if (status >= 200 && status < 300) {
- return true;
+ private static void ensureSuccessfulResponse(int status) {
+ if (status < 200 || status >= 300) {
+ String errorMessage = createErrorMessage(status);
+ LOGGER.log(SEVERE, errorMessage);
+ throw new RuntimeException(errorMessage);
}
-
- LOGGER.log(SEVERE, createErrorMessage(status));
-
- return false;
}
@SuppressWarnings("StringOperationCanBeSimplified")
private static String readResponseBody(HttpURLConnection connection) throws IOException {
try (InputStream responseBody = connection.getInputStream();
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
if (responseBody == null) {
@@ -447,9 +428,7 @@ static Map> getWithOnlyResponseHeaders(String uri, Connecti
try {
connection = connectionFactory.open(uri);
-
- connection.setRequestMethod("GET");
- connection.setRequestProperty(USER_AGENT_KEY, USER_AGENT_VALUE);
+ configureConnection(connection, "GET", null);
if (connection.getResponseCode() == 401) {
Map> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
@@ -478,14 +457,25 @@ static Map> getWithOnlyResponseHeaders(String uri, Connecti
}
}
+ private static void configureConnection(HttpURLConnection connection, String method, Map headers)
+ throws IOException {
+ connection.setRequestMethod(method);
+ connection.setConnectTimeout(HTTP_TIMEOUT_IN_MILLISECONDS);
+ connection.setReadTimeout(HTTP_TIMEOUT_IN_MILLISECONDS);
+ if (headers != null) {
+ headers.forEach(connection::setRequestProperty);
+ }
+ connection.setRequestProperty(USER_AGENT_KEY, USER_AGENT_VALUE);
+ }
+
private static HttpURLConnection openConnection(String uri) {
try {
HttpURLConnection connection = (HttpURLConnection) URI.create(uri).toURL().openConnection();
if (connection instanceof HttpsURLConnection) {
try {
- TrustManagerFactory trustManagerFactory =
- TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ TrustManagerFactory trustManagerFactory
+ = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(JreKeyStoreFactory.getDefaultKeyStore());
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificatesTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificatesTest.java
index 6183339d122b..30a725aea0ef 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificatesTest.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/certificates/KeyVaultCertificatesTest.java
@@ -1260,17 +1260,14 @@ private synchronized void sequence(K key, List> suppliers) {
private void returnValues(K key, List values) {
List> suppliers = new ArrayList<>();
-
for (V value : values) {
suppliers.add(() -> value);
}
-
sequence(key, suppliers);
}
private void throwThenReturn(K key, RuntimeException exception, V value) {
List> suppliers = new ArrayList<>();
-
suppliers.add(() -> {
throw exception;
});
@@ -1284,7 +1281,6 @@ private void answer(K key, Supplier supplier) {
private void answerThenReturn(K key, Supplier supplier, V value) {
List> suppliers = new ArrayList<>();
-
suppliers.add(supplier);
suppliers.add(() -> value);
sequence(key, suppliers);
@@ -1292,31 +1288,23 @@ private void answerThenReturn(K key, Supplier supplier, V value) {
private int callCount(K key) {
AtomicInteger counter;
-
synchronized (this) {
counter = callCounts.get(key);
}
-
return counter == null ? 0 : counter.get();
}
private V invoke(K key) {
Supplier supplier;
-
synchronized (this) {
AtomicInteger counter = callCounts.computeIfAbsent(key, unused -> new AtomicInteger());
-
counter.incrementAndGet();
-
Deque> suppliers = queuedSuppliers.get(key);
-
if (suppliers == null || suppliers.isEmpty()) {
- return null;
+ throw new AssertionError("Unexpected call for key: " + key);
}
-
supplier = suppliers.size() > 1 ? suppliers.poll() : suppliers.peek();
}
-
// The actual supplier invocation happens outside the synchronized block so blocking suppliers used by
// concurrency tests don't serialize unrelated calls against this same script.
return supplier.get();
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/HttpUtilTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/HttpUtilTest.java
index ff3a2b03abf7..23ff93a7bc2b 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/HttpUtilTest.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/HttpUtilTest.java
@@ -58,15 +58,19 @@ public void testHttpUtilGet1() {
@Test
void textGetReturnsSuccessfulResponseBody() throws Exception {
byte[] body = "response".getBytes(StandardCharsets.UTF_8);
- TestHttpURLConnection connection =
- new TestHttpURLConnection("https://example.test/value", 200, body, Collections.emptyMap());
+ TestHttpURLConnection connection
+ = new TestHttpURLConnection("https://example.test/value", 200, body, Collections.emptyMap());
+ Map requestHeaders = new LinkedHashMap<>();
+ requestHeaders.put("x-test", "value");
+ requestHeaders.put(HttpUtil.USER_AGENT_KEY, "caller-value");
- String result = HttpUtil.get("https://example.test/value", Collections.singletonMap("x-test", "value"),
- ignored -> connection);
+ String result = HttpUtil.get("https://example.test/value", requestHeaders, ignored -> connection);
assertEquals("response", result);
assertEquals("value", connection.getRequestProperty("x-test"));
assertEquals(HttpUtil.USER_AGENT_VALUE, connection.getRequestProperty(HttpUtil.USER_AGENT_KEY));
+ assertEquals(HttpUtil.HTTP_TIMEOUT_IN_MILLISECONDS, connection.getConnectTimeout());
+ assertEquals(HttpUtil.HTTP_TIMEOUT_IN_MILLISECONDS, connection.getReadTimeout());
assertTrue(connection.disconnected);
}
@@ -83,33 +87,37 @@ void textGetThrowsForNonSuccessfulResponse() throws Exception {
}
@Test
- void postReturnsNullForNonSuccessfulResponse() throws Exception {
+ void postThrowsForNonSuccessfulResponse() throws Exception {
TestHttpURLConnection connection = new TestHttpURLConnection("https://example.test/value", 403,
"{\"error\":\"forbidden\"}".getBytes(StandardCharsets.UTF_8), Collections.emptyMap());
- String result = HttpUtil
- .post("https://example.test/value", null, "request", "application/json", ignored -> connection);
+ RuntimeException exception = assertThrows(RuntimeException.class, () -> HttpUtil
+ .post("https://example.test/value", null, "request", "application/json", ignored -> connection));
- assertNull(result);
+ assertTrue(exception.getMessage().contains("403"));
assertEquals("request", new String(connection.requestBody.toByteArray(), StandardCharsets.UTF_8));
+ assertEquals(HttpUtil.HTTP_TIMEOUT_IN_MILLISECONDS, connection.getConnectTimeout());
+ assertEquals(HttpUtil.HTTP_TIMEOUT_IN_MILLISECONDS, connection.getReadTimeout());
assertTrue(connection.disconnected);
}
@Test
void authenticationChallengeReturnsCaseInsensitiveHeadersOnlyFor401() throws Exception {
- Map> headers =
- Collections.singletonMap("www-authenticate", Collections.singletonList("Bearer authorization=test"));
- TestHttpURLConnection unauthorized =
- new TestHttpURLConnection("https://example.test/challenge", 401, new byte[0], headers);
+ Map> headers
+ = Collections.singletonMap("www-authenticate", Collections.singletonList("Bearer authorization=test"));
+ TestHttpURLConnection unauthorized
+ = new TestHttpURLConnection("https://example.test/challenge", 401, new byte[0], headers);
- Map> result =
- HttpUtil.getWithOnlyResponseHeaders("https://example.test/challenge", ignored -> unauthorized);
+ Map> result
+ = HttpUtil.getWithOnlyResponseHeaders("https://example.test/challenge", ignored -> unauthorized);
assertEquals("Bearer authorization=test", result.get("WWW-Authenticate").get(0));
+ assertEquals(HttpUtil.HTTP_TIMEOUT_IN_MILLISECONDS, unauthorized.getConnectTimeout());
+ assertEquals(HttpUtil.HTTP_TIMEOUT_IN_MILLISECONDS, unauthorized.getReadTimeout());
assertTrue(unauthorized.disconnected);
- TestHttpURLConnection successful =
- new TestHttpURLConnection("https://example.test/challenge", 200, new byte[0], headers);
+ TestHttpURLConnection successful
+ = new TestHttpURLConnection("https://example.test/challenge", 200, new byte[0], headers);
assertNull(HttpUtil.getWithOnlyResponseHeaders("https://example.test/challenge", ignored -> successful));
assertTrue(successful.disconnected);
}
@@ -124,8 +132,8 @@ void binaryResponsePreservesBodyAndFreshnessHeaders() throws Exception {
headers.put("Expires", Collections.singletonList("Wed, 05 Aug 2026 10:05:00 GMT"));
TestHttpURLConnection connection = new TestHttpURLConnection(200, body, headers);
- HttpUtil.BinaryHttpResponse result =
- HttpUtil.getAiaBytesWithMetadata("https://example.test/cert.crt", ignored -> connection);
+ HttpUtil.BinaryHttpResponse result
+ = HttpUtil.getAiaBytesWithMetadata("https://example.test/cert.crt", ignored -> connection);
assertArrayEquals(body, result.getBody());
assertEquals("public, max-age=300", result.getCacheControl());
@@ -140,12 +148,12 @@ void binaryResponsePreservesBodyAndFreshnessHeaders() throws Exception {
@Test
void binaryResponseForFailureHasNoBodyAndPreservesFreshnessMetadata() throws Exception {
- Map> headers =
- Collections.singletonMap("Cache-Control", Collections.singletonList("max-age=3600"));
+ Map> headers
+ = Collections.singletonMap("Cache-Control", Collections.singletonList("max-age=3600"));
TestHttpURLConnection connection = new TestHttpURLConnection(503, new byte[] { 1 }, headers);
- HttpUtil.BinaryHttpResponse result =
- HttpUtil.getAiaBytesWithMetadata("https://example.test/cert.crt", ignored -> connection);
+ HttpUtil.BinaryHttpResponse result
+ = HttpUtil.getAiaBytesWithMetadata("https://example.test/cert.crt", ignored -> connection);
assertNull(result.getBody());
assertEquals("max-age=3600", result.getCacheControl());
@@ -160,8 +168,8 @@ void binaryResponseCombinesMultipleCacheControlHeaders() throws Exception {
headers.put("cache-control", Arrays.asList("public, max-age=300", "no-store"));
TestHttpURLConnection connection = new TestHttpURLConnection(200, new byte[] { 1 }, headers);
- HttpUtil.BinaryHttpResponse result =
- HttpUtil.getAiaBytesWithMetadata("https://example.test/cert.crt", ignored -> connection);
+ HttpUtil.BinaryHttpResponse result
+ = HttpUtil.getAiaBytesWithMetadata("https://example.test/cert.crt", ignored -> connection);
assertEquals("public, max-age=300, no-store", result.getCacheControl());
}
@@ -172,8 +180,8 @@ void binaryResponseRejectsOversizedContentLength() throws Exception {
Collections.singletonList(String.valueOf(HttpUtil.MAX_AIA_RESPONSE_SIZE_IN_BYTES + 1)));
TestHttpURLConnection connection = new TestHttpURLConnection(200, new byte[] { 1 }, headers);
- HttpUtil.BinaryHttpResponse result =
- HttpUtil.getAiaBytesWithMetadata("https://example.test/cert.crt", ignored -> connection);
+ HttpUtil.BinaryHttpResponse result
+ = HttpUtil.getAiaBytesWithMetadata("https://example.test/cert.crt", ignored -> connection);
assertNull(result.getBody());
assertTrue(connection.disconnected);
@@ -184,8 +192,8 @@ void binaryResponseRejectsStreamThatExceedsMaximumSize() throws Exception {
byte[] body = new byte[HttpUtil.MAX_AIA_RESPONSE_SIZE_IN_BYTES + 1];
TestHttpURLConnection connection = new TestHttpURLConnection(200, body, Collections.emptyMap());
- HttpUtil.BinaryHttpResponse result =
- HttpUtil.getAiaBytesWithMetadata("https://example.test/cert.crt", ignored -> connection);
+ HttpUtil.BinaryHttpResponse result
+ = HttpUtil.getAiaBytesWithMetadata("https://example.test/cert.crt", ignored -> connection);
assertNull(result.getBody());
assertTrue(connection.disconnected);
@@ -195,8 +203,8 @@ void binaryResponseRejectsStreamThatExceedsMaximumSize() throws Exception {
void binaryResponseFollowsHttpToHttpsRedirect() throws Exception {
String sourceUrl = "http://example.test/cert.crt";
String targetUrl = "https://example.test/cert.crt";
- Map> redirectHeaders =
- Collections.singletonMap("Location", Collections.singletonList(targetUrl));
+ Map> redirectHeaders
+ = Collections.singletonMap("Location", Collections.singletonList(targetUrl));
TestHttpURLConnection redirect = new TestHttpURLConnection(sourceUrl, 302, new byte[0], redirectHeaders);
byte[] body = new byte[] { 1, 2, 3 };
TestHttpURLConnection response = new TestHttpURLConnection(targetUrl, 200, body, Collections.emptyMap());
@@ -221,8 +229,8 @@ void binaryResponseFollowsHttpToHttpsRedirect() throws Exception {
void binaryResponsePreservesPathForQueryOnlyRedirect() throws Exception {
String sourceUrl = "https://example.test/certificates/issuer.crt?v=1";
String targetUrl = "https://example.test/certificates/issuer.crt?v=2";
- Map> redirectHeaders =
- Collections.singletonMap("Location", Collections.singletonList("?v=2"));
+ Map> redirectHeaders
+ = Collections.singletonMap("Location", Collections.singletonList("?v=2"));
TestHttpURLConnection redirect = new TestHttpURLConnection(sourceUrl, 302, new byte[0], redirectHeaders);
byte[] body = new byte[] { 1, 2, 3 };
TestHttpURLConnection response = new TestHttpURLConnection(targetUrl, 200, body, Collections.emptyMap());
From 455beae7b69232b0147fc496fb0375296f1b2cd9 Mon Sep 17 00:00:00 2001
From: Victor Colin Amador
Date: Thu, 27 Aug 2026 02:51:01 -0700
Subject: [PATCH 11/16] Added a few comments
---
.../jca/implementation/utils/AccessTokenUtil.java | 6 ++++++
.../jca/implementation/utils/AiaCertificateChainUtil.java | 8 ++++++--
.../keyvault/jca/implementation/utils/HttpUtil.java | 6 ++++++
3 files changed, 18 insertions(+), 2 deletions(-)
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AccessTokenUtil.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AccessTokenUtil.java
index 94a029acdd94..cbf7802488e8 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AccessTokenUtil.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AccessTokenUtil.java
@@ -130,6 +130,7 @@ public static AccessToken getAccessToken(String resource, String aadAuthenticati
return getAccessToken(resource, aadAuthenticationUrl, tenantId, clientId, clientSecret, HttpUtil::post);
}
+ // Overloaded method that allows specifying a custom HttpPoster for testing.
static AccessToken getAccessToken(String resource, String aadAuthenticationUrl, String tenantId, String clientId,
String clientSecret, HttpPoster httpPoster) {
// The client secret is deliberately left out: entering() renders every parameter in clear text.
@@ -168,6 +169,11 @@ static AccessToken getAccessToken(String resource, String aadAuthenticationUrl,
return result;
}
+ /**
+ * Functional interface for making HTTP POST requests.
+ *
+ * Introduced to be used for testing purposes, allowing the HTTP POST behavior to be mocked or overridden.
+ */
@FunctionalInterface
interface HttpPoster {
String post(String uri, Map headers, String body, String contentType);
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java
index c34f9319d97d..db0cc0f8c44e 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java
@@ -602,7 +602,8 @@ static void clearAiaCache() {
/**
* Sets the response loader for AIA requests.
*
- * This can be used to override the default HTTP-based loader, for example in tests.
+ *
Introduced to be used for testing purposes, allowing the HTTP response loading behavior to be mocked or
+ * overridden.
*
* @param loader the response loader to use
*/
@@ -613,7 +614,8 @@ static synchronized void setResponseLoader(AiaResponseLoader loader) {
/**
* Resets the response loader for AIA requests to the default HTTP-based loader.
*
- *
This can be used to undo any overrides set by {@link #setResponseLoader(AiaResponseLoader)}.
+ *
Introduced to be used for testing purposes, allowing the HTTP response loading behavior to be reset to the
+ * default.
*/
static synchronized void resetResponseLoader() {
responseLoader = DEFAULT_RESPONSE_LOADER;
@@ -621,6 +623,8 @@ static synchronized void resetResponseLoader() {
/**
* Functional interface for loading AIA responses.
+ *
+ *
Introduced to be used for testing purposes, allowing the HTTP POST behavior to be mocked or overridden.
*/
@FunctionalInterface
interface AiaResponseLoader {
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/HttpUtil.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/HttpUtil.java
index 04d517502f0c..2e0c0dd15b54 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/HttpUtil.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/HttpUtil.java
@@ -57,6 +57,11 @@ public final class HttpUtil {
private static final int AIA_HTTP_TOTAL_TIMEOUT_IN_MILLISECONDS = 30_000;
private static final int MAX_AIA_REDIRECTS = 5;
+ /**
+ * Functional interface for opening HTTP connections.
+ *
+ *
Introduced to be used for testing purposes, allowing the HTTP connection behavior to be mocked or overridden.
+ */
@FunctionalInterface
interface ConnectionFactory {
HttpURLConnection open(String url) throws IOException;
@@ -423,6 +428,7 @@ public static Map> getWithOnlyResponseHeaders(String uri) {
return getWithOnlyResponseHeaders(uri, HttpUtil::openConnection);
}
+ // Overloaded method that allows specifying a custom ConnectionFactory for testing purposes.
static Map> getWithOnlyResponseHeaders(String uri, ConnectionFactory connectionFactory) {
HttpURLConnection connection = null;
From fd7c85807049cabbedd8004c786209f8b1f1353e Mon Sep 17 00:00:00 2001
From: Victor Colin Amador
Date: Thu, 27 Aug 2026 14:07:04 -0700
Subject: [PATCH 12/16] Reverted formatting merge changes
---
.../messaging/eventhubs/stress/scenarios/EventForwarder.java | 3 ++-
.../azure/messaging/eventhubs/stress/util/TelemetryHelper.java | 2 +-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/sdk/eventhubs/azure-messaging-eventhubs-stress/src/main/java/com/azure/messaging/eventhubs/stress/scenarios/EventForwarder.java b/sdk/eventhubs/azure-messaging-eventhubs-stress/src/main/java/com/azure/messaging/eventhubs/stress/scenarios/EventForwarder.java
index 907ecf0f6873..4fd67886541d 100644
--- a/sdk/eventhubs/azure-messaging-eventhubs-stress/src/main/java/com/azure/messaging/eventhubs/stress/scenarios/EventForwarder.java
+++ b/sdk/eventhubs/azure-messaging-eventhubs-stress/src/main/java/com/azure/messaging/eventhubs/stress/scenarios/EventForwarder.java
@@ -105,7 +105,8 @@ public void run() {
private EventHubProducerAsyncClient getForwardProducer() {
final TokenCredential tokenCredential = new DefaultAzureCredentialBuilder().build();
final EventHubClientBuilder builder = new EventHubClientBuilder()
- .credential(options.getEventHubsFullyQualifiedNamespace(), forwardEventHubName, tokenCredential)
+ .credential(options.getEventHubsFullyQualifiedNamespace(), forwardEventHubName,
+ tokenCredential)
.retryOptions(new AmqpRetryOptions().setTryTimeout(Duration.ofSeconds(10)))
.transportType(options.getAmqpTransportType())
.consumerGroup(options.getEventHubsConsumerGroup());
diff --git a/sdk/eventhubs/azure-messaging-eventhubs-stress/src/main/java/com/azure/messaging/eventhubs/stress/util/TelemetryHelper.java b/sdk/eventhubs/azure-messaging-eventhubs-stress/src/main/java/com/azure/messaging/eventhubs/stress/util/TelemetryHelper.java
index 0f9f6a37a768..393155d8914e 100644
--- a/sdk/eventhubs/azure-messaging-eventhubs-stress/src/main/java/com/azure/messaging/eventhubs/stress/util/TelemetryHelper.java
+++ b/sdk/eventhubs/azure-messaging-eventhubs-stress/src/main/java/com/azure/messaging/eventhubs/stress/util/TelemetryHelper.java
@@ -90,7 +90,7 @@ public TelemetryHelper(Class> scenarioClass) {
*/
private static OpenTelemetry init() {
System.setProperty("otel.java.global-autoconfigure.enabled", "true");
-
+
AutoConfiguredOpenTelemetrySdkBuilder sdkBuilder = AutoConfiguredOpenTelemetrySdk.builder();
String applicationInsightsConnectionString = System.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING");
if (applicationInsightsConnectionString == null) {
From d18315eb253870f03fb326fa1d5ff749f52b9140 Mon Sep 17 00:00:00 2001
From: Victor Colin Amador
Date: Fri, 28 Aug 2026 16:12:56 -0700
Subject: [PATCH 13/16] Updated samples to use HttpUrlConnection
---
.../azure-security-keyvault-jca/README.md | 164 ++++++++++++++++--
.../security/keyvault/jca/SampleUtils.java | 98 -----------
.../jca/TrustSelfSignedServerDelegate.java | 50 ------
.../keyvault/jca/mtls/ClientMTLSSample.java | 92 ++++++++--
.../keyvault/jca/mtls/ServerMTLSSample.java | 9 +
.../keyvault/jca/tls/ClientSSLSample.java | 77 +++++++-
.../keyvault/jca/tls/ServerSSLSample.java | 6 +
7 files changed, 313 insertions(+), 183 deletions(-)
delete mode 100644 sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/SampleUtils.java
delete mode 100644 sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/TrustSelfSignedServerDelegate.java
diff --git a/sdk/keyvault/azure-security-keyvault-jca/README.md b/sdk/keyvault/azure-security-keyvault-jca/README.md
index ed8a36a5a86d..8d508ca54631 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/README.md
+++ b/sdk/keyvault/azure-security-keyvault-jca/README.md
@@ -216,13 +216,17 @@ System.setProperty("azure.keyvault.client-id", ""
System.setProperty("azure.keyvault.client-secret", "");
KeyVaultJcaProvider provider = new KeyVaultJcaProvider();
+// Register the provider before requesting its KeyStore implementation.
Security.addProvider(provider);
+// Load the certificate and private key that identify this server to connecting clients.
KeyStore keyStore = KeyVaultKeyStore.getKeyVaultKeyStoreBySystemProperty();
+// Key managers select the server certificate and private key during each TLS handshake.
KeyManagerFactory managerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
managerFactory.init(keyStore, "".toCharArray());
+// Configure one-way TLS: clients aren't required to present a certificate.
SSLContext context = SSLContext.getInstance("TLS");
context.init(managerFactory.getKeyManagers(), null, null);
@@ -230,11 +234,13 @@ SSLServerSocketFactory socketFactory = context.getServerSocketFactory();
SSLServerSocket serverSocket = (SSLServerSocket) socketFactory.createServerSocket(8765);
while (true) {
+ // Accept a TLS connection and write a minimal HTTP response over it.
SSLSocket socket = (SSLSocket) serverSocket.accept();
System.out.println("Client connected: " + socket.getInetAddress());
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
String body = "Hello, this is server.";
+ // Build a minimal HTTP response and calculate Content-Length from the UTF-8 body bytes.
String response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: "
+ body.getBytes(StandardCharsets.UTF_8).length + "\r\nConnection: close\r\n\r\n" + body;
@@ -247,7 +253,7 @@ while (true) {
**Note:** See [Authentication Methods](#authentication-methods) for configuration details.
#### Client side SSL
-If you are looking to integrate the JCA provider for client side socket connections, see the Apache HTTP client example below.
+If you are looking to integrate the JCA provider for client side socket connections, see the HTTPS URL connection example below.
```java readme-sample-clientSSL
System.setProperty("azure.keyvault.uri", "");
@@ -256,14 +262,47 @@ System.setProperty("azure.keyvault.client-id", ""
System.setProperty("azure.keyvault.client-secret", "");
KeyVaultJcaProvider provider = new KeyVaultJcaProvider();
+// Register the provider before requesting its KeyStore implementation.
Security.addProvider(provider);
KeyStore keyStore = KeyVaultKeyStore.getKeyVaultKeyStoreBySystemProperty();
-// This section initializing SSLContext can be replaced with implementation specific consumption of 'KeyStore',
-// if the library being used has convenience methods for that.
+// Create trust managers from the certificates in the Key Vault-backed KeyStore.
+TrustManagerFactory trustManagerFactory
+ = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+trustManagerFactory.init(keyStore);
+TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
+
+// The local server may use a self-signed certificate. Accept a one-certificate server chain while delegating
+// validation of all other chains to the platform trust manager. Do not use this behavior in production.
+for (int i = 0; i < trustManagers.length; i++) {
+ if (trustManagers[i] instanceof X509TrustManager) {
+ X509TrustManager delegate = (X509TrustManager) trustManagers[i];
+ trustManagers[i] = new X509TrustManager() {
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ delegate.checkClientTrusted(chain, authType);
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ if (chain.length != 1) {
+ delegate.checkServerTrusted(chain, authType);
+ }
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return delegate.getAcceptedIssuers();
+ }
+ };
+ }
+}
+
+// Configure one-way TLS: the client validates the server but doesn't present a client certificate.
SSLContext sslContext = SSLContext.getInstance("TLS");
-TrustManager[] trustManagers = SampleUtils.loadTrustMaterial(keyStore);
sslContext.init(null, trustManagers, null);
String result = null;
@@ -272,19 +311,40 @@ try {
// openConnection will return HttpsURLConnection when the protocol is 'https'.
connection = (HttpsURLConnection) URI.create("https://localhost:8765").toURL().openConnection();
- // Have the HttpsURLConnection use the SSLSocketFactory returned by SSLContext.
+ // Apply the custom trust configuration to this HTTPS connection.
connection.setSSLSocketFactory(sslContext.getSocketFactory());
+ // Allow the sample certificate to use a hostname other than localhost. Do not do this in production.
+ connection.setHostnameVerifier((hostname, session) -> true);
connection.setRequestMethod("GET");
int status = connection.getResponseCode();
if (status == 200) {
- result = SampleUtils.readResponse(connection);
+ // Decode the response using its declared charset, or UTF-8 when no charset is present.
+ Charset responseCharset = StandardCharsets.UTF_8;
+ String contentType = connection.getContentType();
+ if (contentType != null) {
+ Matcher matcher = Pattern.compile("(?i)\\bcharset\\s*=\\s*\"?([^;\\s\"]+)")
+ .matcher(contentType);
+ if (matcher.find()) {
+ responseCharset = Charset.forName(matcher.group(1));
+ }
+ }
+
+ // Read the complete body without changing its line endings.
+ try (Reader reader = new InputStreamReader(connection.getInputStream(), responseCharset)) {
+ StringBuilder responseBody = new StringBuilder();
+ char[] buffer = new char[1024];
+ int read;
+ while ((read = reader.read(buffer)) != -1) {
+ responseBody.append(buffer, 0, read);
+ }
+ result = responseBody.toString();
+ }
} else {
result = "Not success";
}
} catch (IOException ioe) {
ioe.printStackTrace();
- result = "Not success";
} finally {
if (connection != null) {
connection.disconnect();
@@ -301,14 +361,17 @@ If you are looking to integrate the JCA provider to create an SSLServerSocket se
```java readme-sample-serverMTLS
KeyVaultJcaProvider provider = new KeyVaultJcaProvider();
+// Register the provider before requesting its KeyStore implementation.
Security.addProvider(provider);
System.setProperty("azure.keyvault.uri", "");
System.setProperty("azure.keyvault.tenant-id", "");
System.setProperty("azure.keyvault.client-id", "");
System.setProperty("azure.keyvault.client-secret", "");
+// Load the certificate and private key that identify this server to connecting clients.
KeyStore keyStore = KeyVaultKeyStore.getKeyVaultKeyStoreBySystemProperty();
+// Key managers select the server certificate and private key during each mTLS handshake.
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, "".toCharArray());
@@ -316,24 +379,30 @@ System.setProperty("azure.keyvault.uri", "");
System.setProperty("azure.keyvault.tenant-id", "");
System.setProperty("azure.keyvault.client-id", "");
System.setProperty("azure.keyvault.client-secret", "");
+// Load the client certificates that this server trusts.
KeyStore trustStore = KeyVaultKeyStore.getKeyVaultKeyStoreBySystemProperty();
+// Trust managers validate the certificate presented by each client.
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
+// Combine the server identity with the client trust configuration.
SSLContext context = SSLContext.getInstance("TLS");
context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
SSLServerSocketFactory socketFactory = context.getServerSocketFactory();
SSLServerSocket serverSocket = (SSLServerSocket) socketFactory.createServerSocket(8765);
+// Require every client to present a trusted certificate during the TLS handshake.
serverSocket.setNeedClientAuth(true);
while (true) {
+ // Accept an mTLS connection and write a minimal HTTP response over it.
SSLSocket socket = (SSLSocket) serverSocket.accept();
System.out.println("Client connected: " + socket.getInetAddress());
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
String body = "Hello, this is server.";
+ // Build a minimal HTTP response and calculate Content-Length from the UTF-8 body bytes.
String response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: "
+ body.getBytes(StandardCharsets.UTF_8).length + "\r\nConnection: close\r\n\r\n" + body;
@@ -346,55 +415,117 @@ while (true) {
**Note:** See [Authentication Methods](#authentication-methods) for configuration details.
#### Client side mTLS
-If you are looking to integrate the JCA provider for client side socket connections, see the Apache HTTP client example below.
+If you are looking to integrate the JCA provider for client side socket connections, see the HTTPS URL connection example below.
```java readme-sample-clientMTLS
KeyVaultJcaProvider provider = new KeyVaultJcaProvider();
+// Register the provider before requesting its KeyStore implementation.
Security.addProvider(provider);
System.setProperty("azure.keyvault.uri", "");
System.setProperty("azure.keyvault.tenant-id", "");
System.setProperty("azure.keyvault.client-id", "");
System.setProperty("azure.keyvault.client-secret", "");
+// Load the certificate and private key that identify this client to the server.
KeyStore keyStore = KeyVaultKeyStore.getKeyVaultKeyStoreBySystemProperty();
System.setProperty("azure.keyvault.uri", "");
System.setProperty("azure.keyvault.tenant-id", "");
System.setProperty("azure.keyvault.client-id", "");
System.setProperty("azure.keyvault.client-secret", "");
+// Load the server certificates that this client trusts.
KeyStore trustStore = KeyVaultKeyStore.getKeyVaultKeyStoreBySystemProperty();
-// This section initializing SSLContext can be replaced with implementation specific consumption of 'KeyStore',
-// if the library being used has convenience methods for that.
+// Create trust managers from the server trust material.
+TrustManagerFactory trustManagerFactory
+ = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+trustManagerFactory.init(trustStore);
+TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
+
+// The local server may use a self-signed certificate. Accept a one-certificate server chain while delegating
+// validation of all other chains to the platform trust manager. Do not use this behavior in production.
+for (int i = 0; i < trustManagers.length; i++) {
+ if (trustManagers[i] instanceof X509TrustManager) {
+ X509TrustManager delegate = (X509TrustManager) trustManagers[i];
+ trustManagers[i] = new X509TrustManager() {
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ delegate.checkClientTrusted(chain, authType);
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ if (chain.length != 1) {
+ delegate.checkServerTrusted(chain, authType);
+ }
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return delegate.getAcceptedIssuers();
+ }
+ };
+ }
+}
+
+// Create key managers that select the client certificate and private key during the mTLS handshake.
+KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+keyManagerFactory.init(keyStore, "".toCharArray());
+
+// Combine the client identity with the server trust configuration.
SSLContext sslContext = SSLContext.getInstance("TLS");
-TrustManager[] trustManagers = SampleUtils.loadTrustMaterial(keyStore);
-KeyManager[] keyManagers = SampleUtils.loadKeyMaterial(keyStore, "".toCharArray());
-sslContext.init(keyManagers, trustManagers, null);
+sslContext.init(keyManagerFactory.getKeyManagers(), trustManagers, null);
String result = null;
HttpsURLConnection connection = null;
+
try {
// openConnection will return HttpsURLConnection when the protocol is 'https'.
connection = (HttpsURLConnection) URI.create("https://localhost:8765").toURL().openConnection();
- // Have the HttpsURLConnection use the SSLSocketFactory returned by SSLContext.
+ // Apply the custom identity and trust configuration to this HTTPS connection.
connection.setSSLSocketFactory(sslContext.getSocketFactory());
+ // Allow the sample certificate to use a hostname other than localhost. Do not do this in production.
+ connection.setHostnameVerifier((hostname, session) -> true);
connection.setRequestMethod("GET");
int status = connection.getResponseCode();
+
if (status == 200) {
- result = SampleUtils.readResponse(connection);
+ // Decode the response using its declared charset, or UTF-8 when no charset is present.
+ Charset responseCharset = StandardCharsets.UTF_8;
+ String contentType = connection.getContentType();
+ if (contentType != null) {
+ Matcher matcher = Pattern.compile("(?i)\\bcharset\\s*=\\s*\"?([^;\\s\"]+)")
+ .matcher(contentType);
+ if (matcher.find()) {
+ responseCharset = Charset.forName(matcher.group(1));
+ }
+ }
+
+ // Read the complete body without changing its line endings.
+ try (Reader reader = new InputStreamReader(connection.getInputStream(), responseCharset)) {
+ StringBuilder responseBody = new StringBuilder();
+ char[] buffer = new char[1024];
+ int read;
+ while ((read = reader.read(buffer)) != -1) {
+ responseBody.append(buffer, 0, read);
+ }
+ result = responseBody.toString();
+ }
} else {
result = "Not success";
}
} catch (IOException ioe) {
ioe.printStackTrace();
- result = "Not success";
} finally {
if (connection != null) {
connection.disconnect();
}
}
+
System.out.println(result);
```
@@ -737,4 +868,3 @@ This project has adopted the [Microsoft Open Source Code of Conduct][microsoft_c
[microsoft_code_of_conduct]: https://opensource.microsoft.com/codeofconduct/
[non-exportable]: https://learn.microsoft.com/azure/key-vault/certificates/about-certificates#exportable-or-non-exportable-key
-
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/SampleUtils.java b/sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/SampleUtils.java
deleted file mode 100644
index f280959df1ec..000000000000
--- a/sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/SampleUtils.java
+++ /dev/null
@@ -1,98 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-package com.azure.security.keyvault.jca;
-
-import javax.net.ssl.KeyManager;
-import javax.net.ssl.KeyManagerFactory;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.TrustManagerFactory;
-import javax.net.ssl.X509TrustManager;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.nio.charset.StandardCharsets;
-import java.security.KeyStore;
-import java.security.KeyStoreException;
-import java.security.NoSuchAlgorithmException;
-import java.security.UnrecoverableKeyException;
-
-/**
- * Utility methods for samples.
- */
-public final class SampleUtils {
- /**
- * Loads the {@link TrustManager TrustManagers} for the {@link KeyStore}.
- *
- * This wraps {@link X509TrustManager X509TrustManagers} with {@link TrustSelfSignedServerDelegate} to support
- * self-signed certificates.
- *
- * @param keyStore The {@link KeyStore} where {@link TrustManager TrustManagers} will be loaded.
- * @return The {@link TrustManager TrustManagers} that were loaded.
- * @throws NoSuchAlgorithmException If the algorithm used when calling
- * {@link TrustManagerFactory#getInstance(String)} isn't available.
- * @throws KeyStoreException If calling {@link TrustManagerFactory#init(KeyStore)} fails.
- */
- public static TrustManager[] loadTrustMaterial(KeyStore keyStore) throws NoSuchAlgorithmException,
- KeyStoreException {
- TrustManagerFactory tmFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
- tmFactory.init(keyStore);
- TrustManager[] trustManagers = tmFactory.getTrustManagers();
- if (trustManagers != null) {
- for (int i = 0; i < trustManagers.length; i++) {
- TrustManager trustManager = trustManagers[i];
- if (trustManager instanceof X509TrustManager) {
- // Wrap X509TrustManagers with an implementation that trusts self-signed certificates.
- // This doesn't need to be done and is just an example.
- trustManagers[i] = new TrustSelfSignedServerDelegate((X509TrustManager) trustManager);
- }
- }
- }
-
- return trustManagers;
- }
-
- /**
- * Loads the {@link KeyManager KeyManagers} for the {@link KeyStore}.
- *
- * @param keyStore The {@link KeyStore} where {@link KeyManager KeyManagers} will be loaded.
- * @param password The password for recovering {@link KeyManager KeyManagers} in the {@link KeyStore}.
- * @return The {@link KeyManager KeyManagers} that were loaded.
- * @throws NoSuchAlgorithmException If the algorithm used when calling {@link KeyManagerFactory#getInstance(String)}
- * isn't available.
- * @throws KeyStoreException If calling {@link KeyManagerFactory#init(KeyStore, char[])} fails.
- * @throws UnrecoverableKeyException If the {@link KeyManager} can't be recovered when calling
- * {@link KeyManagerFactory#init(KeyStore, char[])}, such as the {@code password is wrong}.
- */
- public static KeyManager[] loadKeyMaterial(KeyStore keyStore, char[] password)
- throws NoSuchAlgorithmException, UnrecoverableKeyException, KeyStoreException {
- KeyManagerFactory kmFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
- kmFactory.init(keyStore, password);
- return kmFactory.getKeyManagers();
- }
-
- /**
- * Reads the {@link HttpURLConnection} response body to a string.
- *
- * @param connection The {@link HttpURLConnection} to read the response body for.
- * @return The response body as a string.
- * @throws IOException If an I/O error occurs while reading the response body.
- */
- @SuppressWarnings("StringOperationCanBeSimplified")
- public static String readResponse(HttpURLConnection connection) throws IOException {
- InputStream response = (connection.getInputStream() != null)
- ? connection.getInputStream()
- : connection.getErrorStream();
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- byte[] buffer = new byte[4096];
- int read;
- while ((read = response.read(buffer)) != -1) {
- outputStream.write(buffer, 0, read);
- }
-
- return new String(outputStream.toByteArray(), StandardCharsets.UTF_8);
- }
-
- private SampleUtils() {
- }
-}
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/TrustSelfSignedServerDelegate.java b/sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/TrustSelfSignedServerDelegate.java
deleted file mode 100644
index 0103ce37b5e3..000000000000
--- a/sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/TrustSelfSignedServerDelegate.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
-// Licensed under the MIT License.
-package com.azure.security.keyvault.jca;
-
-import javax.net.ssl.X509TrustManager;
-import java.security.cert.CertificateException;
-import java.security.cert.X509Certificate;
-import java.util.Objects;
-
-/**
- * Implementation of {@link X509TrustManager} that wraps another {@link X509TrustManager} with a check where self-signed
- * server chains are trusted.
- *
- * This implementation uses basic validation for checking if the chain is self-signed, where it only checks that the
- * chain has a length of one. This validation only applies when running
- * {@link X509TrustManager#checkServerTrusted(X509Certificate[], String)}, and if it passes that method call does not
- * delegate to the wrapped {@link X509TrustManager}.
- *
- * {@link X509TrustManager#checkClientTrusted(X509Certificate[], String)} and
- * {@link X509TrustManager#getAcceptedIssuers()} always delegate to the wrapped {@link X509TrustManager}.
- */
-public final class TrustSelfSignedServerDelegate implements X509TrustManager {
- private final X509TrustManager delegate;
-
- /**
- * Creates a new instance of {@link TrustSelfSignedServerDelegate}.
- *
- * @param delegate The {@link X509TrustManager} that this {@link TrustSelfSignedServerDelegate} will delegate.
- */
- public TrustSelfSignedServerDelegate(X509TrustManager delegate) {
- this.delegate = Objects.requireNonNull(delegate, "'delegate' cannot be null.");
- }
-
- @Override
- public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
- delegate.checkClientTrusted(chain, authType);
- }
-
- @Override
- public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
- if (chain.length != 1) {
- checkServerTrusted(chain, authType);
- }
- }
-
- @Override
- public X509Certificate[] getAcceptedIssuers() {
- return delegate.getAcceptedIssuers();
- }
-}
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/mtls/ClientMTLSSample.java b/sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/mtls/ClientMTLSSample.java
index b3f3711b4a6f..d3003ed55b8a 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/mtls/ClientMTLSSample.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/mtls/ClientMTLSSample.java
@@ -4,16 +4,25 @@
import com.azure.security.keyvault.jca.KeyVaultJcaProvider;
import com.azure.security.keyvault.jca.KeyVaultKeyStore;
-import com.azure.security.keyvault.jca.SampleUtils;
import javax.net.ssl.HttpsURLConnection;
-import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
import java.net.URI;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.Security;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* The ClientMTLS sample.
@@ -23,53 +32,114 @@ public class ClientMTLSSample {
public static void main(String[] args) throws Exception {
// BEGIN: readme-sample-clientMTLS
KeyVaultJcaProvider provider = new KeyVaultJcaProvider();
+ // Register the provider before requesting its KeyStore implementation.
Security.addProvider(provider);
System.setProperty("azure.keyvault.uri", "");
System.setProperty("azure.keyvault.tenant-id", "");
System.setProperty("azure.keyvault.client-id", "");
System.setProperty("azure.keyvault.client-secret", "");
+ // Load the certificate and private key that identify this client to the server.
KeyStore keyStore = KeyVaultKeyStore.getKeyVaultKeyStoreBySystemProperty();
System.setProperty("azure.keyvault.uri", "");
System.setProperty("azure.keyvault.tenant-id", "");
System.setProperty("azure.keyvault.client-id", "");
System.setProperty("azure.keyvault.client-secret", "");
+ // Load the server certificates that this client trusts.
KeyStore trustStore = KeyVaultKeyStore.getKeyVaultKeyStoreBySystemProperty();
- // This section initializing SSLContext can be replaced with implementation specific consumption of 'KeyStore',
- // if the library being used has convenience methods for that.
+ // Create trust managers from the server trust material.
+ TrustManagerFactory trustManagerFactory
+ = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(trustStore);
+ TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
+
+ // The local server may use a self-signed certificate. Accept a one-certificate server chain while delegating
+ // validation of all other chains to the platform trust manager. Do not use this behavior in production.
+ for (int i = 0; i < trustManagers.length; i++) {
+ if (trustManagers[i] instanceof X509TrustManager) {
+ X509TrustManager delegate = (X509TrustManager) trustManagers[i];
+ trustManagers[i] = new X509TrustManager() {
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ delegate.checkClientTrusted(chain, authType);
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ if (chain.length != 1) {
+ delegate.checkServerTrusted(chain, authType);
+ }
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return delegate.getAcceptedIssuers();
+ }
+ };
+ }
+ }
+
+ // Create key managers that select the client certificate and private key during the mTLS handshake.
+ KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ keyManagerFactory.init(keyStore, "".toCharArray());
+
+ // Combine the client identity with the server trust configuration.
SSLContext sslContext = SSLContext.getInstance("TLS");
- TrustManager[] trustManagers = SampleUtils.loadTrustMaterial(keyStore);
- KeyManager[] keyManagers = SampleUtils.loadKeyMaterial(keyStore, "".toCharArray());
- sslContext.init(keyManagers, trustManagers, null);
+ sslContext.init(keyManagerFactory.getKeyManagers(), trustManagers, null);
String result = null;
HttpsURLConnection connection = null;
+
try {
// openConnection will return HttpsURLConnection when the protocol is 'https'.
connection = (HttpsURLConnection) URI.create("https://localhost:8765").toURL().openConnection();
- // Have the HttpsURLConnection use the SSLSocketFactory returned by SSLContext.
+ // Apply the custom identity and trust configuration to this HTTPS connection.
connection.setSSLSocketFactory(sslContext.getSocketFactory());
+ // Allow the sample certificate to use a hostname other than localhost. Do not do this in production.
+ connection.setHostnameVerifier((hostname, session) -> true);
connection.setRequestMethod("GET");
int status = connection.getResponseCode();
+
if (status == 200) {
- result = SampleUtils.readResponse(connection);
+ // Decode the response using its declared charset, or UTF-8 when no charset is present.
+ Charset responseCharset = StandardCharsets.UTF_8;
+ String contentType = connection.getContentType();
+ if (contentType != null) {
+ Matcher matcher = Pattern.compile("(?i)\\bcharset\\s*=\\s*\"?([^;\\s\"]+)")
+ .matcher(contentType);
+ if (matcher.find()) {
+ responseCharset = Charset.forName(matcher.group(1));
+ }
+ }
+
+ // Read the complete body without changing its line endings.
+ try (Reader reader = new InputStreamReader(connection.getInputStream(), responseCharset)) {
+ StringBuilder responseBody = new StringBuilder();
+ char[] buffer = new char[1024];
+ int read;
+ while ((read = reader.read(buffer)) != -1) {
+ responseBody.append(buffer, 0, read);
+ }
+ result = responseBody.toString();
+ }
} else {
result = "Not success";
}
} catch (IOException ioe) {
ioe.printStackTrace();
- result = "Not success";
} finally {
if (connection != null) {
connection.disconnect();
}
}
+
System.out.println(result);
// END: readme-sample-clientMTLS
}
-
}
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/mtls/ServerMTLSSample.java b/sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/mtls/ServerMTLSSample.java
index 3f83b02ed56d..574d322522ec 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/mtls/ServerMTLSSample.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/mtls/ServerMTLSSample.java
@@ -25,14 +25,17 @@ public class ServerMTLSSample {
public static void main(String[] args) throws Exception {
// BEGIN: readme-sample-serverMTLS
KeyVaultJcaProvider provider = new KeyVaultJcaProvider();
+ // Register the provider before requesting its KeyStore implementation.
Security.addProvider(provider);
System.setProperty("azure.keyvault.uri", "");
System.setProperty("azure.keyvault.tenant-id", "");
System.setProperty("azure.keyvault.client-id", "");
System.setProperty("azure.keyvault.client-secret", "");
+ // Load the certificate and private key that identify this server to connecting clients.
KeyStore keyStore = KeyVaultKeyStore.getKeyVaultKeyStoreBySystemProperty();
+ // Key managers select the server certificate and private key during each mTLS handshake.
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(keyStore, "".toCharArray());
@@ -40,24 +43,30 @@ public static void main(String[] args) throws Exception {
System.setProperty("azure.keyvault.tenant-id", "");
System.setProperty("azure.keyvault.client-id", "");
System.setProperty("azure.keyvault.client-secret", "");
+ // Load the client certificates that this server trusts.
KeyStore trustStore = KeyVaultKeyStore.getKeyVaultKeyStoreBySystemProperty();
+ // Trust managers validate the certificate presented by each client.
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
+ // Combine the server identity with the client trust configuration.
SSLContext context = SSLContext.getInstance("TLS");
context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
SSLServerSocketFactory socketFactory = context.getServerSocketFactory();
SSLServerSocket serverSocket = (SSLServerSocket) socketFactory.createServerSocket(8765);
+ // Require every client to present a trusted certificate during the TLS handshake.
serverSocket.setNeedClientAuth(true);
while (true) {
+ // Accept an mTLS connection and write a minimal HTTP response over it.
SSLSocket socket = (SSLSocket) serverSocket.accept();
System.out.println("Client connected: " + socket.getInetAddress());
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
String body = "Hello, this is server.";
+ // Build a minimal HTTP response and calculate Content-Length from the UTF-8 body bytes.
String response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: "
+ body.getBytes(StandardCharsets.UTF_8).length + "\r\nConnection: close\r\n\r\n" + body;
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/tls/ClientSSLSample.java b/sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/tls/ClientSSLSample.java
index cf7d49a6f202..1094d3f1f256 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/tls/ClientSSLSample.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/tls/ClientSSLSample.java
@@ -4,15 +4,24 @@
import com.azure.security.keyvault.jca.KeyVaultJcaProvider;
import com.azure.security.keyvault.jca.KeyVaultKeyStore;
-import com.azure.security.keyvault.jca.SampleUtils;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
import java.net.URI;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.Security;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* The ClientSSL sample.
@@ -27,14 +36,47 @@ public static void main(String[] args) throws Exception {
System.setProperty("azure.keyvault.client-secret", "");
KeyVaultJcaProvider provider = new KeyVaultJcaProvider();
+ // Register the provider before requesting its KeyStore implementation.
Security.addProvider(provider);
KeyStore keyStore = KeyVaultKeyStore.getKeyVaultKeyStoreBySystemProperty();
- // This section initializing SSLContext can be replaced with implementation specific consumption of 'KeyStore',
- // if the library being used has convenience methods for that.
+ // Create trust managers from the certificates in the Key Vault-backed KeyStore.
+ TrustManagerFactory trustManagerFactory
+ = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(keyStore);
+ TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
+
+ // The local server may use a self-signed certificate. Accept a one-certificate server chain while delegating
+ // validation of all other chains to the platform trust manager. Do not use this behavior in production.
+ for (int i = 0; i < trustManagers.length; i++) {
+ if (trustManagers[i] instanceof X509TrustManager) {
+ X509TrustManager delegate = (X509TrustManager) trustManagers[i];
+ trustManagers[i] = new X509TrustManager() {
+ @Override
+ public void checkClientTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ delegate.checkClientTrusted(chain, authType);
+ }
+
+ @Override
+ public void checkServerTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ if (chain.length != 1) {
+ delegate.checkServerTrusted(chain, authType);
+ }
+ }
+
+ @Override
+ public X509Certificate[] getAcceptedIssuers() {
+ return delegate.getAcceptedIssuers();
+ }
+ };
+ }
+ }
+
+ // Configure one-way TLS: the client validates the server but doesn't present a client certificate.
SSLContext sslContext = SSLContext.getInstance("TLS");
- TrustManager[] trustManagers = SampleUtils.loadTrustMaterial(keyStore);
sslContext.init(null, trustManagers, null);
String result = null;
@@ -43,19 +85,40 @@ public static void main(String[] args) throws Exception {
// openConnection will return HttpsURLConnection when the protocol is 'https'.
connection = (HttpsURLConnection) URI.create("https://localhost:8765").toURL().openConnection();
- // Have the HttpsURLConnection use the SSLSocketFactory returned by SSLContext.
+ // Apply the custom trust configuration to this HTTPS connection.
connection.setSSLSocketFactory(sslContext.getSocketFactory());
+ // Allow the sample certificate to use a hostname other than localhost. Do not do this in production.
+ connection.setHostnameVerifier((hostname, session) -> true);
connection.setRequestMethod("GET");
int status = connection.getResponseCode();
if (status == 200) {
- result = SampleUtils.readResponse(connection);
+ // Decode the response using its declared charset, or UTF-8 when no charset is present.
+ Charset responseCharset = StandardCharsets.UTF_8;
+ String contentType = connection.getContentType();
+ if (contentType != null) {
+ Matcher matcher = Pattern.compile("(?i)\\bcharset\\s*=\\s*\"?([^;\\s\"]+)")
+ .matcher(contentType);
+ if (matcher.find()) {
+ responseCharset = Charset.forName(matcher.group(1));
+ }
+ }
+
+ // Read the complete body without changing its line endings.
+ try (Reader reader = new InputStreamReader(connection.getInputStream(), responseCharset)) {
+ StringBuilder responseBody = new StringBuilder();
+ char[] buffer = new char[1024];
+ int read;
+ while ((read = reader.read(buffer)) != -1) {
+ responseBody.append(buffer, 0, read);
+ }
+ result = responseBody.toString();
+ }
} else {
result = "Not success";
}
} catch (IOException ioe) {
ioe.printStackTrace();
- result = "Not success";
} finally {
if (connection != null) {
connection.disconnect();
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/tls/ServerSSLSample.java b/sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/tls/ServerSSLSample.java
index 491c8113c4ae..614bee61948d 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/tls/ServerSSLSample.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/samples/java/com/azure/security/keyvault/jca/tls/ServerSSLSample.java
@@ -29,13 +29,17 @@ public static void main(String[] args) throws Exception {
System.setProperty("azure.keyvault.client-secret", "");
KeyVaultJcaProvider provider = new KeyVaultJcaProvider();
+ // Register the provider before requesting its KeyStore implementation.
Security.addProvider(provider);
+ // Load the certificate and private key that identify this server to connecting clients.
KeyStore keyStore = KeyVaultKeyStore.getKeyVaultKeyStoreBySystemProperty();
+ // Key managers select the server certificate and private key during each TLS handshake.
KeyManagerFactory managerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
managerFactory.init(keyStore, "".toCharArray());
+ // Configure one-way TLS: clients aren't required to present a certificate.
SSLContext context = SSLContext.getInstance("TLS");
context.init(managerFactory.getKeyManagers(), null, null);
@@ -43,11 +47,13 @@ public static void main(String[] args) throws Exception {
SSLServerSocket serverSocket = (SSLServerSocket) socketFactory.createServerSocket(8765);
while (true) {
+ // Accept a TLS connection and write a minimal HTTP response over it.
SSLSocket socket = (SSLSocket) serverSocket.accept();
System.out.println("Client connected: " + socket.getInetAddress());
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
String body = "Hello, this is server.";
+ // Build a minimal HTTP response and calculate Content-Length from the UTF-8 body bytes.
String response = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: "
+ body.getBytes(StandardCharsets.UTF_8).length + "\r\nConnection: close\r\n\r\n" + body;
From 5d3a43dd319660bb3d7fe5b813a5b140335ffc5c Mon Sep 17 00:00:00 2001
From: Victor Colin Amador
Date: Tue, 1 Sep 2026 09:54:29 -0700
Subject: [PATCH 14/16] Fix JCA HTTP migration tests
---
.../keyvault/jca/JreKeyStoreTest.java | 1 +
.../keyvault/jca/ServerSocketTest.java | 1 +
.../implementation/KeyVaultClientTest.java | 48 +++++--------------
3 files changed, 14 insertions(+), 36 deletions(-)
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/JreKeyStoreTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/JreKeyStoreTest.java
index fcca34eb1a77..76b056c7f93b 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/JreKeyStoreTest.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/JreKeyStoreTest.java
@@ -66,6 +66,7 @@ public void testJreKsTrustPeer() throws Exception {
HttpsURLConnection connection = null;
try {
connection = (HttpsURLConnection) URI.create("https://google.com:443").toURL().openConnection();
+ connection.setSSLSocketFactory(sslContext.getSocketFactory());
connection.setRequestMethod("GET");
if (connection.getResponseCode() == 200) {
result = "Success";
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/ServerSocketTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/ServerSocketTest.java
index a83d91e3cb87..6e2c63efcca7 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/ServerSocketTest.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/ServerSocketTest.java
@@ -182,6 +182,7 @@ private String sendRequest(SSLContext sslContext, int port) {
try {
connection = (HttpsURLConnection) URI.create("https://localhost:" + port).toURL().openConnection();
connection.setSSLSocketFactory(sslContext.getSocketFactory());
+ connection.setHostnameVerifier((hostname, session) -> true);
connection.setRequestMethod("GET");
if (connection.getResponseCode() == 204) {
result = "Success";
diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/KeyVaultClientTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/KeyVaultClientTest.java
index aa8407ddd963..7b302732d0df 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/KeyVaultClientTest.java
+++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/KeyVaultClientTest.java
@@ -50,6 +50,8 @@
public class KeyVaultClientTest {
private static final String KEY_VAULT_TEST_URI_GLOBAL = "https://fake.vault.azure.net/";
+ private static final String TEST_ACCESS_TOKEN = "test-token";
+
private static final String CERTIFICATE_ALIAS = "client-cert";
private static final String CERTIFICATE_URI
@@ -62,12 +64,7 @@ public class KeyVaultClientTest {
@Test
public void testGetAliasWithCertificateInfoWith0Page() {
- KeyVaultClient keyVaultClient = new KeyVaultClient(KEY_VAULT_TEST_URI_GLOBAL, null) {
- @Override
- String httpGet(String uri, Map headers) {
- return "fakeValue";
- }
- };
+ KeyVaultClient keyVaultClient = new TestKeyVaultClient(TEST_ACCESS_TOKEN, false, (uri, headers) -> "fakeValue");
assertEquals(0, keyVaultClient.getAliases().size());
}
@@ -83,12 +80,8 @@ public void testGetAliasWithCertificateInfoWith1Page() {
String certificateListResultString = JsonConverterUtil.toJson(certificateListResult);
- KeyVaultClient keyVaultClient = new KeyVaultClient(KEY_VAULT_TEST_URI_GLOBAL, null) {
- @Override
- String httpGet(String uri, Map headers) {
- return certificateListResultString;
- }
- };
+ KeyVaultClient keyVaultClient
+ = new TestKeyVaultClient(TEST_ACCESS_TOKEN, false, (uri, headers) -> certificateListResultString);
List result = keyVaultClient.getAliases();
assertEquals(1, result.size());
@@ -119,12 +112,8 @@ public void testGetAliasWithCertificateInfoWith2Pages() {
String certificateListResultString = JsonConverterUtil.toJson(certificateListResult);
String certificateListResultStringNext = JsonConverterUtil.toJson(certificateListResultNext);
- KeyVaultClient keyVaultClient = new KeyVaultClient(KEY_VAULT_TEST_URI_GLOBAL, null) {
- @Override
- String httpGet(String uri, Map headers) {
- return "fakeNextLink".equals(uri) ? certificateListResultStringNext : certificateListResultString;
- }
- };
+ KeyVaultClient keyVaultClient = new TestKeyVaultClient(TEST_ACCESS_TOKEN, false, (uri,
+ headers) -> "fakeNextLink".equals(uri) ? certificateListResultStringNext : certificateListResultString);
List result = keyVaultClient.getAliases();
assertEquals(3, result.size());
@@ -153,12 +142,8 @@ public void testGetAliasFiltersOutDisabledCertificate() {
String certificateListResultString = JsonConverterUtil.toJson(certificateListResult);
- KeyVaultClient keyVaultClient = new KeyVaultClient(KEY_VAULT_TEST_URI_GLOBAL, null) {
- @Override
- String httpGet(String uri, Map headers) {
- return certificateListResultString;
- }
- };
+ KeyVaultClient keyVaultClient
+ = new TestKeyVaultClient(TEST_ACCESS_TOKEN, false, (uri, headers) -> certificateListResultString);
List result = keyVaultClient.getAliases();
assertEquals(1, result.size());
@@ -184,12 +169,8 @@ public void testGetAliasKeepsEnabledAndAttributelessCertificates() {
String certificateListResultString = JsonConverterUtil.toJson(certificateListResult);
- KeyVaultClient keyVaultClient = new KeyVaultClient(KEY_VAULT_TEST_URI_GLOBAL, null) {
- @Override
- String httpGet(String uri, Map headers) {
- return certificateListResultString;
- }
- };
+ KeyVaultClient keyVaultClient
+ = new TestKeyVaultClient(TEST_ACCESS_TOKEN, false, (uri, headers) -> certificateListResultString);
List result = keyVaultClient.getAliases();
assertEquals(2, result.size());
@@ -206,12 +187,7 @@ public void testGetAliasFiltersDisabledCertificateFromRawResponse() {
+ "{\"id\":\"https://fake.vault.azure.net/certificates/client-cert-unused\","
+ "\"attributes\":{\"enabled\":false,\"nbf\":1783324860,\"exp\":1814861460}}]," + "\"nextLink\":null}";
- KeyVaultClient keyVaultClient = new KeyVaultClient(KEY_VAULT_TEST_URI_GLOBAL, null) {
- @Override
- String httpGet(String uri, Map headers) {
- return rawResponse;
- }
- };
+ KeyVaultClient keyVaultClient = new TestKeyVaultClient(TEST_ACCESS_TOKEN, false, (uri, headers) -> rawResponse);
List result = keyVaultClient.getAliases();
assertEquals(1, result.size());
From 9abdb606931cd3d739b25f85aad4e778c106fbc3 Mon Sep 17 00:00:00 2001
From: Victor Colin Amador
Date: Tue, 1 Sep 2026 09:56:08 -0700
Subject: [PATCH 15/16] Clean up JCA HTTP migration dependencies
---
sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md | 1 +
sdk/keyvault/azure-security-keyvault-jca/pom.xml | 11 -----------
.../implementation/utils/AiaCertificateChainTest.java | 6 ++++--
3 files changed, 5 insertions(+), 13 deletions(-)
diff --git a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md
index 366ac061f8d7..f6d584c06a75 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md
+++ b/sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md
@@ -16,6 +16,7 @@
### Other Changes
- Added system property `azure.keyvault.jca.disable-aia-download` to disable automatic AIA chain completion. AIA chain completion downloads certificates from URLs embedded in certificate extensions, so this allows locked-down environments to prevent those outbound HTTP(S) requests, mitigating potential SSRF-like attack vectors when loading untrusted certificates. The value is captured when each Key Vault client is initialized and retained for lazy certificate-chain loading, so multiple keystores can use different settings without overwriting one another. Set to `true` to disable (defaults to `false`).
- Added `KeyVaultJcaPropertyNames` as the central source for the system property names supported by the Azure Key Vault JCA provider. ([#50163](https://github.com/Azure/azure-sdk-for-java/pull/50163))
+- Replaced Apache HttpClient 5 with the JDK `HttpURLConnection`, removing the Apache HttpClient and SLF4J runtime dependencies.
## 2.12.0 (2026-07-24)
diff --git a/sdk/keyvault/azure-security-keyvault-jca/pom.xml b/sdk/keyvault/azure-security-keyvault-jca/pom.xml
index 6605164f7a88..493645c99a3a 100644
--- a/sdk/keyvault/azure-security-keyvault-jca/pom.xml
+++ b/sdk/keyvault/azure-security-keyvault-jca/pom.xml
@@ -48,12 +48,6 @@
1.5.1
true