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
58 changes: 24 additions & 34 deletions auth0-api-java/src/main/java/com/auth0/JWTValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,20 @@
import com.auth0.jwk.Jwk;
import com.auth0.jwk.JwkProvider;
import com.auth0.jwk.JwkProviderBuilder;
import com.auth0.jwk.UrlJwkProvider;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.models.HttpRequestInfo;
import com.auth0.models.RequestContext;
import com.auth0.telemetry.Telemetry;

import java.net.MalformedURLException;
import java.net.URL;
import java.security.interfaces.RSAPublicKey;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;

/**
Expand All @@ -36,9 +38,9 @@ class JWTValidator {
static final String JWKS_CACHE_PREFIX = "jwks:";

private final AuthOptions authOptions;
private final JwkProvider jwkProvider;
private final OidcDiscoveryFetcher discoveryFetcher;
private final AuthCache<Object> cache;
private final Map<String, String> telemetryHeaders;

/**
* Creates a JWT validator with domain and audience.
Expand All @@ -52,50 +54,42 @@ class JWTValidator {
}

this.authOptions = authOptions;
this.jwkProvider = authOptions.getDomain() != null
? new UrlJwkProvider(authOptions.getDomain())
: null;
this.telemetryHeaders = telemetryHeaders(authOptions);
this.cache = resolveCache(authOptions);
this.discoveryFetcher = new OidcDiscoveryFetcher(this.cache);
}

/**
* Creates a JWT validator with domain, audience, and a custom JwkProvider.
*
* @param authOptions Authentication options containing domain and audience
* @param jwkProvider Custom JwkProvider for key retrieval
*/
JWTValidator(AuthOptions authOptions, JwkProvider jwkProvider) {
if (authOptions == null) {
throw new IllegalArgumentException("AuthOptions cannot be null");
}
if (jwkProvider == null) {
throw new IllegalArgumentException("JwkProvider cannot be null");
}
this.authOptions = authOptions;
this.jwkProvider = jwkProvider;
this.cache = resolveCache(authOptions);
this.discoveryFetcher = new OidcDiscoveryFetcher(this.cache);
this.discoveryFetcher = new OidcDiscoveryFetcher(this.cache, authOptions.getTelemetry());
}

/**
* Creates a JWT validator with all dependencies injectable (primarily for
* testing).
*
* @param authOptions Authentication options
* @param jwkProvider Custom JwkProvider for key retrieval
* @param discoveryFetcher Custom OIDC discovery fetcher
*/
JWTValidator(AuthOptions authOptions, JwkProvider jwkProvider, OidcDiscoveryFetcher discoveryFetcher) {
JWTValidator(AuthOptions authOptions, OidcDiscoveryFetcher discoveryFetcher) {
if (authOptions == null) {
throw new IllegalArgumentException("AuthOptions cannot be null");
}
this.authOptions = authOptions;
this.jwkProvider = jwkProvider;
this.telemetryHeaders = telemetryHeaders(authOptions);
this.cache = resolveCache(authOptions);
this.discoveryFetcher = discoveryFetcher != null
? discoveryFetcher
: new OidcDiscoveryFetcher(this.cache);
: new OidcDiscoveryFetcher(this.cache, authOptions.getTelemetry());
}

/**
* Builds the {@code Auth0-Client} header map for JWKS requests, or an empty
* map when no telemetry value is available.
*/
private static Map<String, String> telemetryHeaders(AuthOptions authOptions) {
Map<String, String> headers = new HashMap<>();
Telemetry telemetry = authOptions.getTelemetry();
String value = telemetry != null ? telemetry.getValue() : null;
if (value != null) {
headers.put(Telemetry.HEADER_NAME, value);
}
return headers;
}

/**
Expand Down Expand Up @@ -245,10 +239,6 @@ public AuthOptions getAuthOptions() {
return authOptions;
}

public JwkProvider getJwkProvider() {
return jwkProvider;
}

/**
* Performs OIDC Discovery for the given issuer URL
* <p>
Expand Down Expand Up @@ -286,7 +276,7 @@ private JwkProvider getOrCreateJwkProvider(String jwksUri) throws VerifyAccessTo
}

try {
JwkProvider provider = new JwkProviderBuilder(new URL(jwksUri)).build();
JwkProvider provider = new JwkProviderBuilder(new URL(jwksUri)).headers(telemetryHeaders).build();
cache.put(cacheKey, provider);
return provider;
} catch (MalformedURLException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.auth0.exception.VerifyAccessTokenException;
import com.auth0.models.OidcMetadata;
import com.auth0.telemetry.Telemetry;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

Expand Down Expand Up @@ -41,6 +42,7 @@ class OidcDiscoveryFetcher implements Closeable {
private final AuthCache<Object> cache;
private final CloseableHttpClient httpClient;
private final boolean ownsHttpClient;
private final String telemetryHeader;

/**
* Creates a fetcher with the provided cache and the default HTTP client.
Expand All @@ -49,7 +51,17 @@ class OidcDiscoveryFetcher implements Closeable {
* @param cache the unified cache instance
*/
OidcDiscoveryFetcher(AuthCache<Object> cache) {
this(cache, HttpClients.createDefault(), true);
this(cache, HttpClients.createDefault(), true, null);
}

/**
* Creates a fetcher with the provided cache, default HTTP client, and telemetry.
*
* @param cache the unified cache instance
* @param telemetry the telemetry identity whose header rides on discovery requests
*/
OidcDiscoveryFetcher(AuthCache<Object> cache, Telemetry telemetry) {
this(cache, HttpClients.createDefault(), true, telemetry);
}

/**
Expand All @@ -60,10 +72,23 @@ class OidcDiscoveryFetcher implements Closeable {
* @param httpClient the HTTP client to use for discovery requests
*/
OidcDiscoveryFetcher(AuthCache<Object> cache, CloseableHttpClient httpClient) {
this(cache, httpClient, false);
this(cache, httpClient, false, null);
}

/**
* Creates a fetcher with the provided cache, a custom HTTP client, and telemetry.
* The caller retains ownership of the HTTP client and is responsible for closing it.
*
* @param cache the unified cache instance
* @param httpClient the HTTP client to use for discovery requests
* @param telemetry the telemetry identity whose header rides on discovery requests
*/
OidcDiscoveryFetcher(AuthCache<Object> cache, CloseableHttpClient httpClient, Telemetry telemetry) {
this(cache, httpClient, false, telemetry);
}

private OidcDiscoveryFetcher(AuthCache<Object> cache, CloseableHttpClient httpClient, boolean ownsHttpClient) {
private OidcDiscoveryFetcher(AuthCache<Object> cache, CloseableHttpClient httpClient,
boolean ownsHttpClient, Telemetry telemetry) {
if (cache == null) {
throw new IllegalArgumentException("cache must not be null");
}
Expand All @@ -73,6 +98,7 @@ private OidcDiscoveryFetcher(AuthCache<Object> cache, CloseableHttpClient httpCl
this.cache = cache;
this.httpClient = httpClient;
this.ownsHttpClient = ownsHttpClient;
this.telemetryHeader = telemetry != null ? telemetry.getValue() : null;
}

/**
Expand Down Expand Up @@ -106,6 +132,9 @@ private OidcMetadata doFetch(String issuerUrl) throws VerifyAccessTokenException
try {
HttpGet request = new HttpGet(discoveryUrl);
request.setHeader("Accept", "application/json");
if (telemetryHeader != null) {
request.setHeader(Telemetry.HEADER_NAME, telemetryHeader);
}

try (CloseableHttpResponse response = httpClient.execute(request)) {
int statusCode = response.getStatusLine().getStatusCode();
Expand Down
30 changes: 30 additions & 0 deletions auth0-api-java/src/main/java/com/auth0/models/AuthOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.auth0.DomainResolver;
import com.auth0.AuthCache;
import com.auth0.enums.DPoPMode;
import com.auth0.telemetry.Telemetry;
import com.auth0.telemetry.TelemetryProvider;

import java.util.ArrayList;
import java.util.Collections;
Expand All @@ -21,6 +23,7 @@ public class AuthOptions {
private final int cacheMaxEntries;
private final long cacheTtlSeconds;
private final AuthCache<Object> cache;
private final Telemetry telemetry;

public AuthOptions(Builder builder) {
this.domain = builder.domain;
Expand All @@ -37,6 +40,7 @@ public AuthOptions(Builder builder) {
this.cacheMaxEntries = builder.cacheMaxEntries;
this.cacheTtlSeconds = builder.cacheTtlSeconds;
this.cache = builder.cache;
this.telemetry = builder.telemetry != null ? builder.telemetry : TelemetryProvider.getDefault();
}

public String getDomain() {
Expand Down Expand Up @@ -112,6 +116,17 @@ public AuthCache<Object> getCache() {
return cache;
}

/**
* Returns the telemetry identity reported via the {@code Auth0-Client} header.
* Defaults to the core {@code auth0-api-java} identity when not overridden by a
* wrapper library.
*
* @return the {@link Telemetry} identity (never null)
*/
public Telemetry getTelemetry() {
return telemetry;
}

public static class Builder {
private String domain;
private List<String> domains;
Expand All @@ -125,6 +140,7 @@ public static class Builder {
private int cacheMaxEntries = 100;
private long cacheTtlSeconds = 600;
private AuthCache<Object> cache;
private Telemetry telemetry;

public Builder domain(String domain) {
this.domain = domain;
Expand Down Expand Up @@ -249,6 +265,20 @@ public Builder cache(AuthCache<Object> cache) {
return this;
}

/**
* Overrides the telemetry identity reported via the {@code Auth0-Client}
* header. Wrapper libraries use this to report themselves as the top-level
* SDK while nesting the core {@code auth0-api-java} version in {@code env}.
* When not set, the core identity is used.
*
* @param telemetry the telemetry identity
* @return this builder
*/
public Builder telemetry(Telemetry telemetry) {
this.telemetry = telemetry;
return this;
}

public AuthOptions build() {
if (domains != null && !domains.isEmpty() && domainsResolver != null) {
throw new IllegalArgumentException(
Expand Down
88 changes: 88 additions & 0 deletions auth0-api-java/src/main/java/com/auth0/telemetry/Telemetry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.auth0.telemetry;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* Immutable Auth0-Client telemetry payload.
*
* <p>Produces the Base64url-encoded value for the {@code Auth0-Client} header,
* following the Auth0 SDK convention:
* {@code {"name":..,"version":..,"env":{"java":..,"auth0-api-java":..}}}.
*
* <p>A wrapper library (e.g. the Spring Boot integration) reports itself as
* {@code name}/{@code version} and passes the core library version so it lands
* in {@code env} under {@link #CORE_LIBRARY_KEY}.
*/
public final class Telemetry {

public static final String HEADER_NAME = "Auth0-Client";
static final String CORE_LIBRARY_KEY = "auth0-api-java";

private final String value;

/**
* @param name the SDK name to report (required; a null name yields a
* null header value that callers must skip)
* @param version the reporting SDK's version, or null to omit
* @param coreVersion the core {@code auth0-api-java} version to nest in
* {@code env}, or null when the core library is itself
* the reporter
*/
public Telemetry(String name, String version, String coreVersion) {
this.value = name == null ? null : encode(build(name, version, coreVersion));
}

/** @return the Base64url header value, or null if no name was provided */
public String getValue() {
return value;
}

private static Map<String, Object> build(String name, String version, String coreVersion) {
Map<String, Object> env = new LinkedHashMap<>();
if (coreVersion != null) {
env.put(CORE_LIBRARY_KEY, coreVersion);
}
env.put("java", System.getProperty("java.specification.version", "unknown"));

Map<String, Object> payload = new LinkedHashMap<>();
payload.put("name", name);
if (version != null) {
payload.put("version", version);
}
payload.put("env", env);
return payload;
}

private static String encode(Map<String, Object> payload) {
String json = toJson(payload);
return Base64.getUrlEncoder().withoutPadding()
.encodeToString(json.getBytes(StandardCharsets.UTF_8));
}

@SuppressWarnings("unchecked")
private static String toJson(Map<String, Object> map) {
StringBuilder sb = new StringBuilder("{");
boolean first = true;
for (Map.Entry<String, Object> e : map.entrySet()) {
if (!first) {
sb.append(",");
}
first = false;
sb.append("\"").append(escape(e.getKey())).append("\":");
Object v = e.getValue();
if (v instanceof Map) {
sb.append(toJson((Map<String, Object>) v));
} else {
sb.append("\"").append(escape(String.valueOf(v))).append("\"");
}
}
return sb.append("}").toString();
}

private static String escape(String s) {
return s.replace("\\", "\\\\").replace("\"", "\\\"");
}
}
Loading
Loading