Skip to content

Commit 98c8052

Browse files
committed
Feat: Add Configurable http client
1 parent 6e492b6 commit 98c8052

10 files changed

Lines changed: 745 additions & 17 deletions

EXAMPLES.md

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,141 @@ JwkProvider provider = new JwkProviderBuilder("https://samples.auth0.com/")
4444
.build();
4545
```
4646

47+
### Configure a custom HTTP client
48+
49+
The `httpClient()` builder method lets you replace the default `java.net.URLConnection`-based HTTP transport with any HTTP library. This solves four common requirements: custom TLS, authenticated proxies, Cache-Control header access, and HTTP/2.
50+
51+
> When `httpClient()` is set, `proxied()`, `timeouts()`, and `headers()` are ignored — the custom client has full control over the HTTP layer.
52+
53+
#### Custom TLS
54+
55+
Force TLS 1.3 for JWKS calls without affecting the rest of your JVM:
56+
57+
```java
58+
SSLContext tls13 = SSLContext.getInstance("TLSv1.3");
59+
tls13.init(null, null, null);
60+
61+
JwksHttpClient tlsClient = url -> {
62+
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
63+
conn.setSSLSocketFactory(tls13.getSocketFactory());
64+
conn.setRequestProperty("Accept", "application/json");
65+
try (InputStream in = conn.getInputStream()) {
66+
String body = new String(in.readAllBytes(), StandardCharsets.UTF_8);
67+
return new JwksHttpResponse(body, conn.getHeaderFields());
68+
}
69+
};
70+
71+
JwkProvider provider = new JwkProviderBuilder("https://samples.auth0.com/")
72+
.httpClient(tlsClient)
73+
.build();
74+
```
75+
76+
> **Note:** TLS 1.3 requires Java 11+ or a provider like [Conscrypt](https://github.com/google/conscrypt) on Java 8.
77+
78+
#### Authenticated Proxy
79+
80+
Use OkHttp to authenticate with a corporate proxy that requires credentials:
81+
82+
```java
83+
OkHttpClient okHttp = new OkHttpClient.Builder()
84+
.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy.corp.com", 8080)))
85+
.proxyAuthenticator((route, response) ->
86+
response.request().newBuilder()
87+
.header("Proxy-Authorization", Credentials.basic("user", "pass"))
88+
.build())
89+
.connectTimeout(Duration.ofSeconds(5))
90+
.readTimeout(Duration.ofSeconds(10))
91+
.build();
92+
93+
JwksHttpClient proxyClient = url -> {
94+
Request request = new Request.Builder().url(url).build();
95+
try (Response response = okHttp.newCall(request).execute()) {
96+
return new JwksHttpResponse(
97+
response.body().string(),
98+
response.headers().toMultimap()
99+
);
100+
}
101+
};
102+
103+
JwkProvider provider = new JwkProviderBuilder("https://samples.auth0.com/")
104+
.httpClient(proxyClient)
105+
.build();
106+
```
107+
108+
#### Cache-Control Headers
109+
110+
Response headers (including `Cache-Control`) are now accessible via `JwksHttpResponse`:
111+
112+
```java
113+
JwksHttpClient headerAwareClient = url -> {
114+
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
115+
conn.setRequestProperty("Accept", "application/json");
116+
try (InputStream in = conn.getInputStream()) {
117+
String body = new String(in.readAllBytes(), StandardCharsets.UTF_8);
118+
JwksHttpResponse response = new JwksHttpResponse(body, conn.getHeaderFields());
119+
120+
// Headers are now available for inspection
121+
String cacheControl = response.getHeaderValue("Cache-Control");
122+
// e.g., "max-age=3600"
123+
124+
return response;
125+
}
126+
};
127+
128+
JwkProvider provider = new JwkProviderBuilder("https://samples.auth0.com/")
129+
.httpClient(headerAwareClient)
130+
.build();
131+
```
132+
133+
#### HTTP/2 Support
134+
135+
**Using Java 11+ HttpClient:**
136+
137+
```java
138+
java.net.http.HttpClient http2Client = java.net.http.HttpClient.newBuilder()
139+
.version(java.net.http.HttpClient.Version.HTTP_2)
140+
.connectTimeout(Duration.ofSeconds(5))
141+
.build();
142+
143+
JwksHttpClient h2Client = url -> {
144+
java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder(url.toURI())
145+
.header("Accept", "application/json")
146+
.GET()
147+
.build();
148+
java.net.http.HttpResponse<String> response = http2Client.send(
149+
request, java.net.http.HttpResponse.BodyHandlers.ofString());
150+
return new JwksHttpResponse(response.body(), response.headers().map());
151+
};
152+
153+
JwkProvider provider = new JwkProviderBuilder("https://samples.auth0.com/")
154+
.httpClient(h2Client)
155+
.build();
156+
```
157+
158+
**Using OkHttp (Java 8 compatible):**
159+
160+
```java
161+
OkHttpClient okHttp = new OkHttpClient.Builder()
162+
.protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
163+
.connectTimeout(Duration.ofSeconds(5))
164+
.readTimeout(Duration.ofSeconds(10))
165+
.build();
166+
167+
JwksHttpClient okClient = url -> {
168+
Request request = new Request.Builder().url(url).build();
169+
try (Response response = okHttp.newCall(request).execute()) {
170+
return new JwksHttpResponse(
171+
response.body().string(),
172+
response.headers().toMultimap()
173+
);
174+
}
175+
};
176+
177+
JwkProvider provider = new JwkProviderBuilder("https://samples.auth0.com/")
178+
.httpClient(okClient)
179+
.build();
180+
```
181+
47182
See the [JwkProviderBuilder JavaDocs](https://javadoc.io/doc/com.auth0/jwks-rsa/latest/com/auth0/jwk/JwkProviderBuilder.html) for all available configurations.
48183

49184
## Error handling
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.auth0.jwk;
2+
3+
import java.io.BufferedReader;
4+
import java.io.IOException;
5+
import java.io.InputStream;
6+
import java.io.InputStreamReader;
7+
import java.net.Proxy;
8+
import java.net.URL;
9+
import java.net.URLConnection;
10+
import java.nio.charset.StandardCharsets;
11+
import java.util.Collections;
12+
import java.util.List;
13+
import java.util.Map;
14+
15+
/**
16+
* Default {@link JwksHttpClient} implementation using {@link java.net.URLConnection}.
17+
*
18+
* <p>This preserves the exact HTTP behavior the library had before the pluggable
19+
* client interface was introduced. It is used automatically when no custom
20+
* {@link JwksHttpClient} is provided to the builder.</p>
21+
*/
22+
final class DefaultJwksHttpClient implements JwksHttpClient {
23+
24+
private final Integer connectTimeout;
25+
private final Integer readTimeout;
26+
private final Proxy proxy;
27+
private final Map<String, String> headers;
28+
29+
/**
30+
* Creates a default HTTP client with the given configuration.
31+
*
32+
* @param connectTimeout connection timeout in milliseconds (null for system default)
33+
* @param readTimeout read timeout in milliseconds (null for system default)
34+
* @param proxy proxy server to use (null for direct connection)
35+
* @param headers request headers to send (null defaults to Accept: application/json)
36+
*/
37+
DefaultJwksHttpClient(Integer connectTimeout, Integer readTimeout,
38+
Proxy proxy, Map<String, String> headers) {
39+
this.connectTimeout = connectTimeout;
40+
this.readTimeout = readTimeout;
41+
this.proxy = proxy;
42+
this.headers = (headers != null) ? headers :
43+
Collections.singletonMap("Accept", "application/json");
44+
}
45+
46+
@Override
47+
public JwksHttpResponse fetch(URL url) throws IOException {
48+
final URLConnection c = (proxy == null) ? url.openConnection() : url.openConnection(proxy);
49+
50+
if (connectTimeout != null) {
51+
c.setConnectTimeout(connectTimeout);
52+
}
53+
if (readTimeout != null) {
54+
c.setReadTimeout(readTimeout);
55+
}
56+
57+
for (Map.Entry<String, String> entry : headers.entrySet()) {
58+
c.setRequestProperty(entry.getKey(), entry.getValue());
59+
}
60+
61+
String body;
62+
try (InputStream in = c.getInputStream();
63+
BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
64+
StringBuilder sb = new StringBuilder();
65+
String line;
66+
while ((line = reader.readLine()) != null) {
67+
sb.append(line);
68+
}
69+
body = sb.toString();
70+
}
71+
72+
Map<String, List<String>> responseHeaders = c.getHeaderFields();
73+
return new JwksHttpResponse(body, responseHeaders);
74+
}
75+
}

src/main/java/com/auth0/jwk/JwkProviderBuilder.java

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public class JwkProviderBuilder {
2424
private BucketImpl bucket;
2525
private boolean rateLimited;
2626
private Map<String, String> headers;
27+
private JwksHttpClient httpClient;
2728

2829
/**
2930
* Creates a new Builder with the given URL where to load the jwks from.
@@ -166,13 +167,55 @@ public JwkProviderBuilder headers(Map<String, String> headers) {
166167
return this;
167168
}
168169

170+
/**
171+
* Sets a custom HTTP client for fetching JWKS.
172+
*
173+
* <p>When a custom client is provided, it takes precedence over configurations set via
174+
* {@link #proxied(Proxy)}, {@link #timeouts(int, int)}, and {@link #headers(Map)} — those
175+
* settings are only used by the default HTTP client.</p>
176+
*
177+
* <p>Example using OkHttp:</p>
178+
* <pre>{@code
179+
* OkHttpClient ok = new OkHttpClient.Builder()
180+
* .proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy.corp", 8080)))
181+
* .proxyAuthenticator((route, resp) -> resp.request().newBuilder()
182+
* .header("Proxy-Authorization", Credentials.basic("user", "pass")).build())
183+
* .protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
184+
* .build();
185+
*
186+
* JwksHttpClient client = url -> {
187+
* Request req = new Request.Builder().url(url).build();
188+
* try (Response resp = ok.newCall(req).execute()) {
189+
* return new JwksHttpResponse(resp.body().string(), resp.headers().toMultimap());
190+
* }
191+
* };
192+
*
193+
* JwkProvider provider = new JwkProviderBuilder(domain)
194+
* .httpClient(client)
195+
* .build();
196+
* }</pre>
197+
*
198+
* @param httpClient the custom HTTP client to use for fetching JWKS
199+
* @return the builder
200+
* @see JwksHttpClient
201+
*/
202+
public JwkProviderBuilder httpClient(JwksHttpClient httpClient) {
203+
this.httpClient = httpClient;
204+
return this;
205+
}
206+
169207
/**
170208
* Creates a {@link JwkProvider}
171209
*
172210
* @return a newly created {@link JwkProvider}
173211
*/
174212
public JwkProvider build() {
175-
JwkProvider urlProvider = new UrlJwkProvider(url, connectTimeout, readTimeout, proxy, headers);
213+
JwkProvider urlProvider;
214+
if (this.httpClient != null) {
215+
urlProvider = new UrlJwkProvider(url, this.httpClient);
216+
} else {
217+
urlProvider = new UrlJwkProvider(url, connectTimeout, readTimeout, proxy, headers);
218+
}
176219
if (this.rateLimited) {
177220
urlProvider = new RateLimitedJwkProvider(urlProvider, bucket);
178221
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.auth0.jwk;
2+
3+
import java.io.IOException;
4+
import java.net.URL;
5+
6+
/**
7+
* Abstraction for fetching JWKS JSON over HTTP.
8+
*
9+
* <p>Implement this interface to control how the library makes HTTP requests.
10+
* This allows customization of TLS settings, proxy authentication, HTTP version,
11+
* and any other transport concern.</p>
12+
*
13+
* <p>This is a functional interface, so a lambda can be used:</p>
14+
* <pre>{@code
15+
* JwksHttpClient client = url -> {
16+
* // Use any HTTP library (OkHttp, Apache HC, Java 11 HttpClient, etc.)
17+
* Request req = new Request.Builder().url(url).build();
18+
* try (Response resp = okHttp.newCall(req).execute()) {
19+
* return new JwksHttpResponse(resp.body().string(), resp.headers().toMultimap());
20+
* }
21+
* };
22+
*
23+
* JwkProvider provider = new JwkProviderBuilder(domain)
24+
* .httpClient(client)
25+
* .build();
26+
* }</pre>
27+
*
28+
* <p>If no custom client is provided, the library uses a default implementation
29+
* based on {@link java.net.URLConnection}.</p>
30+
*/
31+
@FunctionalInterface
32+
public interface JwksHttpClient {
33+
34+
/**
35+
* Fetch the JWKS JSON from the given URL.
36+
*
37+
* <p>Implementations should:</p>
38+
* <ul>
39+
* <li>Make an HTTP GET request to the URL</li>
40+
* <li>Return the response body and headers wrapped in a {@link JwksHttpResponse}</li>
41+
* <li>Throw {@link IOException} on any network or protocol error</li>
42+
* </ul>
43+
*
44+
* @param url the JWKS endpoint URL
45+
* @return the HTTP response containing the body and headers
46+
* @throws IOException on any network or protocol error
47+
*/
48+
JwksHttpResponse fetch(URL url) throws IOException;
49+
}

0 commit comments

Comments
 (0)