Skip to content
Open
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
![Java Version](https://img.shields.io/badge/java-8%2B-blue)
![License](https://img.shields.io/badge/license-MIT-green)

A comprehensive Java library for Auth0 JWT authentication with built-in **DPoP (Demonstration of Proof-of-Possession)** support. This project provides Spring Boot integration for secure API development.
A comprehensive Java library for Auth0 JWT authentication with built-in **DPoP (Demonstration of Proof-of-Possession)** and **Multi-Custom Domain (MCD)** support. This project provides Spring Boot integration for secure API development.

## 🏗️ Architecture Overview

Expand Down Expand Up @@ -49,6 +49,8 @@ It provides:

- JWT validation with Auth0 JWKS integration
- DPoP proof validation per [RFC 9449](https://datatracker.ietf.org/doc/html/rfc9449)
- Multi-Custom Domain (MCD) support — static domain lists, or dynamic resolution at request time
- Extensible caching — pluggable `AuthCache` interface for distributed backends (Redis, Memcached)
- Flexible authentication strategies


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import com.auth0.models.AuthToken;
import com.auth0.models.AuthenticationContext;
import com.auth0.models.HttpRequestInfo;
import com.auth0.validators.DPoPProofValidator;
import com.auth0.validators.JWTValidator;

import java.util.HashMap;
import java.util.List;
Expand All @@ -28,21 +26,21 @@ protected AbstractAuthentication(JWTValidator jwtValidator, TokenExtractor extra
/**
* Concrete method to validate Bearer token headers and JWT claims.
*/
protected DecodedJWT validateBearerToken(Map<String, String> headers, HttpRequestInfo httpRequestInfo) throws BaseAuthException {
AuthToken authToken = extractor.extractBearer(headers);
return jwtValidator.validateToken(authToken.getAccessToken(), headers, httpRequestInfo);
protected DecodedJWT validateBearerToken(HttpRequestInfo httpRequestInfo) throws BaseAuthException {
AuthToken authToken = extractor.extractBearer(httpRequestInfo.getHeaders());
return jwtValidator.validateToken(authToken.getAccessToken(), httpRequestInfo);
}

/**
* Concrete method to validate DPoP token headers, JWT claims, and proof.
*/
protected DecodedJWT validateDpopTokenAndProof(Map<String, String> headers, HttpRequestInfo requestInfo)
protected DecodedJWT validateDpopTokenAndProof(HttpRequestInfo requestInfo)
throws BaseAuthException {

AuthValidatorHelper.validateHttpMethodAndHttpUrl(requestInfo);

AuthToken authToken = extractor.extractDPoPProofAndDPoPToken(headers);
DecodedJWT decodedJwtToken = jwtValidator.validateToken(authToken.getAccessToken(), headers, requestInfo);
AuthToken authToken = extractor.extractDPoPProofAndDPoPToken(requestInfo.getHeaders());
DecodedJWT decodedJwtToken = jwtValidator.validateToken(authToken.getAccessToken(), requestInfo);

dpopProofValidator.validate(authToken.getProof(), decodedJwtToken, requestInfo);

Expand All @@ -52,9 +50,7 @@ protected DecodedJWT validateDpopTokenAndProof(Map<String, String> headers, Http
/**
* Main abstract method for each concrete strategy.
*/
public abstract AuthenticationContext authenticate(
Map<String, String> headers,
HttpRequestInfo requestInfo
public abstract AuthenticationContext authenticate(HttpRequestInfo requestInfo
) throws BaseAuthException;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.models.AuthenticationContext;
import com.auth0.models.HttpRequestInfo;
import com.auth0.validators.DPoPProofValidator;
import com.auth0.validators.JWTValidator;

import java.util.Map;
class AllowedDPoPAuthentication extends AbstractAuthentication {

public AllowedDPoPAuthentication(JWTValidator jwtValidator,
Expand All @@ -20,30 +17,27 @@ public AllowedDPoPAuthentication(JWTValidator jwtValidator,

/**
* Authenticates the request when DPoP Mode is Allowed (Accepts both DPoP and Bearer tokens) .
* @param headers request headers
* @param requestInfo HTTP request info
* @return AuthenticationContext with JWT claims
* @throws BaseAuthException if validation fails
*/
@Override
public AuthenticationContext authenticate(Map<String, String> headers, HttpRequestInfo requestInfo)
public AuthenticationContext authenticate(HttpRequestInfo requestInfo)
throws BaseAuthException {

String scheme = "";

try{
Map<String, String> normalizedHeader = normalize(headers);

scheme = extractor.getScheme(normalizedHeader);
scheme = extractor.getScheme(requestInfo.getHeaders());

if (scheme.equalsIgnoreCase(AuthConstants.BEARER_SCHEME)) {
DecodedJWT jwtToken = validateBearerToken(normalizedHeader, requestInfo);
AuthValidatorHelper.validateNoDpopPresence(normalizedHeader, jwtToken);
DecodedJWT jwtToken = validateBearerToken(requestInfo);
AuthValidatorHelper.validateNoDpopPresence(requestInfo.getHeaders(), jwtToken);
return buildContext(jwtToken);
}

if (scheme.equalsIgnoreCase(AuthConstants.DPOP_SCHEME)) {
DecodedJWT decodedJWT = validateDpopTokenAndProof(normalizedHeader, requestInfo);
DecodedJWT decodedJWT = validateDpopTokenAndProof(requestInfo);
return buildContext(decodedJWT);
}

Expand Down
65 changes: 65 additions & 0 deletions auth0-api-java/src/main/java/com/auth0/AuthCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.auth0;

/**
* Cache abstraction for storing authentication-related data such as
* OIDC discovery metadata and JWKS providers.
* <p>
* The SDK ships with a default in-memory LRU implementation
* ({@link InMemoryAuthCache}). Users can implement this interface
* to plug in distributed cache backends (e.g., Redis, Memcached) without
* breaking changes to the SDK's public API.
* </p>
*
* <p>
* A single {@code AuthCache<Object>} instance can serve as a unified cache
* for both discovery metadata and JWKS providers by using key prefixes:
* </p>
* <ul>
* <li>{@code discovery:{issuerUrl}} — OIDC discovery metadata</li>
* <li>{@code jwks:{jwksUri}} — JwkProvider instances</li>
* </ul>
*
* <h3>Thread Safety</h3>
* <p>
* All implementations <b>must</b> be thread-safe.
* </p>
*
* @param <V> the type of cached values
*/
public interface AuthCache<V> {

/**
* Retrieves a value from the cache.
*
* @param key the cache key
* @return the cached value, or {@code null} if not present or expired
*/
V get(String key);

/**
* Stores a value in the cache with the cache's default TTL.
*
* @param key the cache key
* @param value the value to cache
*/
void put(String key, V value);

/**
* Removes a specific entry from the cache.
*
* @param key the cache key to remove
*/
void remove(String key);

/**
* Removes all entries from the cache.
*/
void clear();

/**
* Returns the number of entries currently in the cache.
*
* @return the cache size
*/
int size();
}
9 changes: 2 additions & 7 deletions auth0-api-java/src/main/java/com/auth0/AuthClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
import com.auth0.models.AuthenticationContext;
import com.auth0.models.AuthOptions;
import com.auth0.models.HttpRequestInfo;
import com.auth0.validators.DPoPProofValidator;
import com.auth0.validators.JWTValidator;

import java.util.Map;

public class AuthClient {

Expand Down Expand Up @@ -45,12 +41,11 @@ public static AuthClient from(AuthOptions options) {

/**
* Verifies the incoming request headers and HTTP request info.
* @param headers request headers
* @param requestInfo HTTP request info
* @return AuthenticationContext with JWT claims
* @throws BaseAuthException if verification fails
*/
public AuthenticationContext verifyRequest(Map<String, String> headers, HttpRequestInfo requestInfo) throws BaseAuthException {
return orchestrator.process(headers, requestInfo);
public AuthenticationContext verifyRequest(HttpRequestInfo requestInfo) throws BaseAuthException {
return orchestrator.process(requestInfo);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.auth0;

public class AuthConstants {
class AuthConstants {
public static final String AUTHORIZATION_HEADER = "authorization";
public static final String DPOP_HEADER = "dpop";
public static final String BEARER_SCHEME = "bearer";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ public AuthenticationOrchestrator(AbstractAuthentication authStrategy) {
this.authStrategy = authStrategy;
}

public AuthenticationContext process(Map<String, String> headers, HttpRequestInfo requestInfo)
public AuthenticationContext process(HttpRequestInfo requestInfo)
throws BaseAuthException {
return authStrategy.authenticate(headers, requestInfo);
return authStrategy.authenticate(requestInfo);
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
package com.auth0.validators;
package com.auth0;

import com.auth0.exception.*;
import com.auth0.jwt.interfaces.DecodedJWT;

import java.util.*;

/**
* Utility class for JWT claim validation
*
* Provides functionality to validate JWT claims including scopes and custom
* claim checks.
* Utility class for JWT claim validation. Provides functionality to validate JWT claims including scopes and custom claim checks.
* This is the Java equivalent of the TypeScript claim validation utilities.
*/
class ClaimValidator {
Expand All @@ -27,13 +24,11 @@ static Set<String> getClaimValues(DecodedJWT jwt, String claimName) throws BaseA
throw new VerifyAccessTokenException("Required claim is missing");
}

// Case 1: space-separated string
String strValue = jwt.getClaim(claimName).asString();
if (strValue != null) {
return new HashSet<>(Arrays.asList(strValue.trim().split("\\s+")));
}

// Case 2: list of strings
List<String> listValue = jwt.getClaim(claimName).asList(String.class);
if (listValue != null) {
return new HashSet<>(listValue);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.auth0.validators;
package com.auth0;

import com.auth0.exception.BaseAuthException;
import com.auth0.exception.InvalidDpopProofException;
Expand All @@ -20,13 +20,13 @@
import java.time.Instant;
import java.util.*;

public class DPoPProofValidator {
class DPoPProofValidator {

private final AuthOptions options;
private final ObjectMapper objectMapper = new ObjectMapper();;


public DPoPProofValidator(AuthOptions options) {
DPoPProofValidator(AuthOptions options) {
this.options = options;
}

Expand All @@ -38,7 +38,7 @@ public DPoPProofValidator(AuthOptions options) {
* @param requestInfo HTTP request info: method and URL
* @throws BaseAuthException if the DPoP proof is invalid.
*/
public void validate(String dpopProof, DecodedJWT decodedJwtToken, HttpRequestInfo requestInfo)
void validate(String dpopProof, DecodedJWT decodedJwtToken, HttpRequestInfo requestInfo)
throws BaseAuthException {

DecodedJWT proofJwt = decodeDPoP(dpopProof);
Expand Down Expand Up @@ -197,7 +197,7 @@ String calculateJwkThumbprint(Map<String, Object> jwk) throws BaseAuthException
}
}

public static ECPublicKey convertJwkToEcPublicKey(Map<String, Object> jwkMap)
static ECPublicKey convertJwkToEcPublicKey(Map<String, Object> jwkMap)
throws JwkException {

Jwk jwk = Jwk.fromValues(jwkMap);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.models.AuthenticationContext;
import com.auth0.models.HttpRequestInfo;
import com.auth0.validators.JWTValidator;

import java.util.Map;

class DisabledDPoPAuthentication extends AbstractAuthentication {

Expand All @@ -17,18 +14,16 @@ public DisabledDPoPAuthentication(JWTValidator jwtValidator, TokenExtractor extr

/**
* Authenticates the request when DPoP Mode is Disabled (Accepts only Bearer tokens) .
* @param headers request headers
* @param requestInfo HTTP request info
* @return AuthenticationContext with JWT claims
* @throws BaseAuthException if validation fails
*/
@Override
public AuthenticationContext authenticate(Map<String, String> headers, HttpRequestInfo requestInfo)
public AuthenticationContext authenticate(HttpRequestInfo requestInfo)
throws BaseAuthException {

Map<String, String> normalizedHeader = normalize(headers);
try {
DecodedJWT jwt = validateBearerToken(normalizedHeader, requestInfo);
DecodedJWT jwt = validateBearerToken(requestInfo);

return buildContext(jwt);
} catch (BaseAuthException ex){
Expand Down
44 changes: 44 additions & 0 deletions auth0-api-java/src/main/java/com/auth0/DomainResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.auth0;

import com.auth0.models.RequestContext;

import java.util.List;

/**
* Functional interface for dynamically resolving allowed issuer domains
* based on the incoming request context.
* <p>
* Used in multi-custom-domain (MCD) scenarios where the set of valid issuers
* cannot be determined statically at configuration time. The resolver receives
* a {@link RequestContext} containing the request URL, headers, and the
* unverified token issuer, and returns the list of allowed issuer domains.
* </p>
*
* <pre>{@code
* AuthOptions options = new AuthOptions.Builder()
* .domainsResolver(context -> {
* String host = context.getHeaders().get("host");
* return lookupIssuersForHost(host);
* })
* .audience("https://api.example.com")
* .build();
* }</pre>
*
* @see RequestContext
* @see com.auth0.models.AuthOptions.Builder#domainsResolver(DomainResolver)
*/
@FunctionalInterface
public interface DomainResolver {

/**
* Resolves the list of allowed issuer domains for the given request context.
*
* @param context the request context containing URL, headers, and unverified
* token issuer
* @return a list of allowed issuer domain strings (e.g.,
* {@code ["https://tenant1.auth0.com/"]});
* may return {@code null} or an empty list if no domains can be
* resolved
*/
List<String> resolveDomains(RequestContext context);
}
Loading
Loading