Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,116 @@ JwkProvider provider = new JwkProviderBuilder("https://samples.auth0.com/")
.build();
```

### Configure a custom HTTP client

The `httpClient()` builder method lets you replace the default `java.net.URLConnection`-based HTTP transport with any HTTP library. This solves three common requirements: custom TLS, authenticated proxies, and HTTP/2.

> When `httpClient()` is set, `proxied()`, `timeouts()`, and `headers()` are ignored — the custom client has full control over the HTTP layer.

#### Custom TLS

Force TLS 1.3 for JWKS calls without affecting the rest of your JVM:

```java
SSLContext tls13 = SSLContext.getInstance("TLSv1.3");
tls13.init(null, null, null);

JwksHttpClient tlsClient = url -> {
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(tls13.getSocketFactory());
conn.setRequestProperty("Accept", "application/json");
try (InputStream in = conn.getInputStream()) {
String body = new String(in.readAllBytes(), StandardCharsets.UTF_8);
return new JwksHttpResponse(body, conn.getHeaderFields());
}
};

JwkProvider provider = new JwkProviderBuilder("https://samples.auth0.com/")
.httpClient(tlsClient)
.build();
```

> **Note:** TLS 1.3 requires Java 11+ or a provider like [Conscrypt](https://github.com/google/conscrypt) on Java 8.

#### Authenticated Proxy

Use OkHttp to authenticate with a corporate proxy that requires credentials:

```java
OkHttpClient okHttp = new OkHttpClient.Builder()
.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy.corp.com", 8080)))
.proxyAuthenticator((route, response) ->
response.request().newBuilder()
.header("Proxy-Authorization", Credentials.basic("user", "pass"))
.build())
.connectTimeout(Duration.ofSeconds(5))
.readTimeout(Duration.ofSeconds(10))
.build();

JwksHttpClient proxyClient = url -> {
Request request = new Request.Builder().url(url).build();
try (Response response = okHttp.newCall(request).execute()) {
return new JwksHttpResponse(
response.body().string(),
response.headers().toMultimap()
);
}
};

JwkProvider provider = new JwkProviderBuilder("https://samples.auth0.com/")
.httpClient(proxyClient)
.build();
```

#### HTTP/2 Support

**Using Java 11+ HttpClient:**

```java
java.net.http.HttpClient http2Client = java.net.http.HttpClient.newBuilder()
.version(java.net.http.HttpClient.Version.HTTP_2)
.connectTimeout(Duration.ofSeconds(5))
.build();

JwksHttpClient h2Client = url -> {
java.net.http.HttpRequest request = java.net.http.HttpRequest.newBuilder(url.toURI())
.header("Accept", "application/json")
.GET()
.build();
java.net.http.HttpResponse<String> response = http2Client.send(
request, java.net.http.HttpResponse.BodyHandlers.ofString());
return new JwksHttpResponse(response.body(), response.headers().map());
};

JwkProvider provider = new JwkProviderBuilder("https://samples.auth0.com/")
.httpClient(h2Client)
.build();
```

**Using OkHttp (Java 8 compatible):**

```java
OkHttpClient okHttp = new OkHttpClient.Builder()
.protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
.connectTimeout(Duration.ofSeconds(5))
.readTimeout(Duration.ofSeconds(10))
.build();

JwksHttpClient okClient = url -> {
Request request = new Request.Builder().url(url).build();
try (Response response = okHttp.newCall(request).execute()) {
return new JwksHttpResponse(
response.body().string(),
response.headers().toMultimap()
);
}
};

JwkProvider provider = new JwkProviderBuilder("https://samples.auth0.com/")
.httpClient(okClient)
.build();
```

See the [JwkProviderBuilder JavaDocs](https://javadoc.io/doc/com.auth0/jwks-rsa/latest/com/auth0/jwk/JwkProviderBuilder.html) for all available configurations.

## Error handling
Expand Down
75 changes: 75 additions & 0 deletions src/main/java/com/auth0/jwk/DefaultJwksHttpClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.auth0.jwk;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Proxy;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
* Default {@link JwksHttpClient} implementation using {@link java.net.URLConnection}.
*
* <p>This preserves the exact HTTP behavior the library had before the pluggable
* client interface was introduced. It is used automatically when no custom
* {@link JwksHttpClient} is provided to the builder.</p>
*/
final class DefaultJwksHttpClient implements JwksHttpClient {

private final Integer connectTimeout;
private final Integer readTimeout;
private final Proxy proxy;
private final Map<String, String> headers;

/**
* Creates a default HTTP client with the given configuration.
*
* @param connectTimeout connection timeout in milliseconds (null for system default)
* @param readTimeout read timeout in milliseconds (null for system default)
* @param proxy proxy server to use (null for direct connection)
* @param headers request headers to send (null defaults to Accept: application/json)
*/
DefaultJwksHttpClient(Integer connectTimeout, Integer readTimeout,
Proxy proxy, Map<String, String> headers) {
this.connectTimeout = connectTimeout;
this.readTimeout = readTimeout;
this.proxy = proxy;
this.headers = (headers != null) ? headers :
Collections.singletonMap("Accept", "application/json");
}

@Override
public JwksHttpResponse fetch(URL url) throws IOException {
final URLConnection c = (proxy == null) ? url.openConnection() : url.openConnection(proxy);

if (connectTimeout != null) {
c.setConnectTimeout(connectTimeout);
}
if (readTimeout != null) {
c.setReadTimeout(readTimeout);
}

for (Map.Entry<String, String> entry : headers.entrySet()) {
c.setRequestProperty(entry.getKey(), entry.getValue());
}

String body;
try (InputStream in = c.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
body = sb.toString();
}

Map<String, List<String>> responseHeaders = c.getHeaderFields();
return new JwksHttpResponse(body, responseHeaders);
}
}
45 changes: 44 additions & 1 deletion src/main/java/com/auth0/jwk/JwkProviderBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class JwkProviderBuilder {
private BucketImpl bucket;
private boolean rateLimited;
private Map<String, String> headers;
private JwksHttpClient httpClient;

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

/**
* Sets a custom HTTP client for fetching JWKS.
*
* <p>When a custom client is provided, it takes precedence over configurations set via
* {@link #proxied(Proxy)}, {@link #timeouts(int, int)}, and {@link #headers(Map)} — those
* settings are only used by the default HTTP client.</p>
*
* <p>Example using OkHttp:</p>
* <pre>{@code
* OkHttpClient ok = new OkHttpClient.Builder()
* .proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("proxy.corp", 8080)))
* .proxyAuthenticator((route, resp) -> resp.request().newBuilder()
* .header("Proxy-Authorization", Credentials.basic("user", "pass")).build())
* .protocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1))
* .build();
*
* JwksHttpClient client = url -> {
* Request req = new Request.Builder().url(url).build();
* try (Response resp = ok.newCall(req).execute()) {
* return new JwksHttpResponse(resp.body().string(), resp.headers().toMultimap());
* }
* };
*
* JwkProvider provider = new JwkProviderBuilder(domain)
* .httpClient(client)
* .build();
* }</pre>
*
* @param httpClient the custom HTTP client to use for fetching JWKS
* @return the builder
* @see JwksHttpClient
*/
public JwkProviderBuilder httpClient(JwksHttpClient httpClient) {
this.httpClient = httpClient;
return this;
}

/**
* Creates a {@link JwkProvider}
*
* @return a newly created {@link JwkProvider}
*/
public JwkProvider build() {
JwkProvider urlProvider = new UrlJwkProvider(url, connectTimeout, readTimeout, proxy, headers);
JwkProvider urlProvider;
if (this.httpClient != null) {
urlProvider = new UrlJwkProvider(url, this.httpClient);
} else {
urlProvider = new UrlJwkProvider(url, connectTimeout, readTimeout, proxy, headers);
}
if (this.rateLimited) {
urlProvider = new RateLimitedJwkProvider(urlProvider, bucket);
}
Expand Down
49 changes: 49 additions & 0 deletions src/main/java/com/auth0/jwk/JwksHttpClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.auth0.jwk;

import java.io.IOException;
import java.net.URL;

/**
* Abstraction for fetching JWKS JSON over HTTP.
*
* <p>Implement this interface to control how the library makes HTTP requests.
* This allows customization of TLS settings, proxy authentication, HTTP version,
* and any other transport concern.</p>
*
* <p>This is a functional interface, so a lambda can be used:</p>
* <pre>{@code
* JwksHttpClient client = url -> {
* // Use any HTTP library (OkHttp, Apache HC, Java 11 HttpClient, etc.)
* Request req = new Request.Builder().url(url).build();
* try (Response resp = okHttp.newCall(req).execute()) {
* return new JwksHttpResponse(resp.body().string(), resp.headers().toMultimap());
* }
* };
*
* JwkProvider provider = new JwkProviderBuilder(domain)
* .httpClient(client)
* .build();
* }</pre>
*
* <p>If no custom client is provided, the library uses a default implementation
* based on {@link java.net.URLConnection}.</p>
*/
@FunctionalInterface
public interface JwksHttpClient {

/**
* Fetch the JWKS JSON from the given URL.
*
* <p>Implementations should:</p>
* <ul>
* <li>Make an HTTP GET request to the URL</li>
* <li>Return the response body and headers wrapped in a {@link JwksHttpResponse}</li>
* <li>Throw {@link IOException} on any network or protocol error</li>
* </ul>
*
* @param url the JWKS endpoint URL
* @return the HTTP response containing the body and headers
* @throws IOException on any network or protocol error
*/
JwksHttpResponse fetch(URL url) throws IOException;
}
74 changes: 74 additions & 0 deletions src/main/java/com/auth0/jwk/JwksHttpResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.auth0.jwk;

import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
* Represents the HTTP response from a JWKS endpoint.
* Contains both the JSON body and the response headers.
*/
public final class JwksHttpResponse {

private final String body;
private final Map<String, List<String>> headers;

/**
* Creates a new response with body and headers.
*
* @param body the response body (JWKS JSON)
* @param headers the response headers (e.g., Cache-Control)
*/
public JwksHttpResponse(String body, Map<String, List<String>> headers) {
this.body = body;
this.headers = (headers != null) ? headers : Collections.<String, List<String>>emptyMap();
}

/**
* Creates a new response with body only (no headers).
*
* @param body the response body (JWKS JSON)
*/
public JwksHttpResponse(String body) {
this(body, Collections.<String, List<String>>emptyMap());
}

/**
* Returns the response body as a string (the JWKS JSON).
*
* @return the response body
*/
public String getBody() {
return body;
}

/**
* Returns all response headers.
*
* @return an unmodifiable map of header names to their values
*/
public Map<String, List<String>> getHeaders() {
return headers;
}

/**
* Returns the first value of a response header (case-insensitive lookup).
*
* <p>Example usage:</p>
* <pre>{@code
* String cacheControl = response.getHeaderValue("Cache-Control");
* }</pre>
*
* @param name the header name (case-insensitive)
* @return the first header value, or null if not present
*/
public String getHeaderValue(String name) {
for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
if (entry.getKey() != null && entry.getKey().equalsIgnoreCase(name)) {
List<String> values = entry.getValue();
return (values != null && !values.isEmpty()) ? values.get(0) : null;
}
}
return null;
}
}
Loading
Loading