Skip to content

Commit 264a291

Browse files
committed
Feat: Added MCD Support
1 parent b101a38 commit 264a291

29 files changed

Lines changed: 2557 additions & 149 deletions
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.auth0;
2+
3+
import com.auth0.models.RequestContext;
4+
5+
import java.util.List;
6+
7+
/**
8+
* Functional interface for dynamically resolving allowed issuer domains
9+
* based on the incoming request context.
10+
* <p>
11+
* Used in multi-custom-domain (MCD) scenarios where the set of valid issuers
12+
* cannot be determined statically at configuration time. The resolver receives
13+
* a {@link RequestContext} containing the request URL, headers, and the
14+
* unverified token issuer, and returns the list of allowed issuer domains.
15+
* </p>
16+
*
17+
* <pre>{@code
18+
* AuthOptions options = new AuthOptions.Builder()
19+
* .domainsResolver(context -> {
20+
* String host = context.getHeaders().get("host");
21+
* return lookupIssuersForHost(host);
22+
* })
23+
* .audience("https://api.example.com")
24+
* .build();
25+
* }</pre>
26+
*
27+
* @see RequestContext
28+
* @see com.auth0.models.AuthOptions.Builder#domainsResolver(DomainResolver)
29+
*/
30+
@FunctionalInterface
31+
public interface DomainResolver {
32+
33+
/**
34+
* Resolves the list of allowed issuer domains for the given request context.
35+
*
36+
* @param context the request context containing URL, headers, and unverified
37+
* token issuer
38+
* @return a list of allowed issuer domain strings (e.g.,
39+
* {@code ["https://tenant1.auth0.com/"]});
40+
* may return {@code null} or an empty list if no domains can be
41+
* resolved
42+
*/
43+
List<String> resolveDomains(RequestContext context);
44+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.auth0.cache;
2+
3+
/**
4+
* Cache abstraction for storing authentication-related data such as
5+
* OIDC discovery metadata and JWKS providers.
6+
* <p>
7+
* The SDK ships with a default in-memory LRU implementation
8+
* ({@link InMemoryAuthCache}). Developers can implement this interface
9+
* to plug in distributed cache backends (e.g., Redis, Memcached) without
10+
* breaking changes to the SDK's public API.
11+
* </p>
12+
*
13+
* <h3>Unified cache with key prefixes</h3>
14+
* <p>
15+
* A single {@code AuthCache<Object>} instance can serve as a unified cache
16+
* for both discovery metadata and JWKS providers by using key prefixes:
17+
* </p>
18+
* <ul>
19+
* <li>{@code discovery:{issuerUrl}} — OIDC discovery metadata</li>
20+
* <li>{@code jwks:{jwksUri}} — JwkProvider instances</li>
21+
* </ul>
22+
*
23+
* <h3>Thread Safety</h3>
24+
* <p>
25+
* All implementations <b>must</b> be thread-safe.
26+
* </p>
27+
*
28+
* @param <V> the type of cached values
29+
*/
30+
public interface AuthCache<V> {
31+
32+
/**
33+
* Retrieves a value from the cache.
34+
*
35+
* @param key the cache key
36+
* @return the cached value, or {@code null} if not present or expired
37+
*/
38+
V get(String key);
39+
40+
/**
41+
* Stores a value in the cache with the cache's default TTL.
42+
*
43+
* @param key the cache key
44+
* @param value the value to cache
45+
*/
46+
void put(String key, V value);
47+
48+
/**
49+
* Removes a specific entry from the cache.
50+
*
51+
* @param key the cache key to remove
52+
*/
53+
void remove(String key);
54+
55+
/**
56+
* Removes all entries from the cache.
57+
*/
58+
void clear();
59+
60+
/**
61+
* Returns the number of entries currently in the cache.
62+
*
63+
* @return the cache size
64+
*/
65+
int size();
66+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
package com.auth0.cache;
2+
3+
import java.util.LinkedHashMap;
4+
import java.util.Map;
5+
import java.util.concurrent.locks.ReentrantReadWriteLock;
6+
7+
/**
8+
* Thread-safe, in-memory LRU cache with TTL expiration.
9+
* <p>
10+
* This is the default {@link AuthCache} implementation shipped with the SDK.
11+
* It uses a {@link LinkedHashMap} in access-order mode for LRU eviction and
12+
* per-entry timestamps for TTL enforcement.
13+
* </p>
14+
*
15+
* <h3>Configuration</h3>
16+
* <ul>
17+
* <li><b>maxEntries</b> — maximum number of entries; LRU eviction when exceeded
18+
* (default: 100)</li>
19+
* <li><b>ttlSeconds</b> — time-to-live per entry in seconds (default: 600 = 10
20+
* minutes)</li>
21+
* </ul>
22+
*
23+
* <h3>Thread Safety</h3>
24+
* <p>
25+
* Uses a {@link ReentrantReadWriteLock} so concurrent reads do not block each
26+
* other,
27+
* while writes acquire exclusive access.
28+
* </p>
29+
*
30+
* @param <V> the type of cached values
31+
*/
32+
public class InMemoryAuthCache<V> implements AuthCache<V> {
33+
34+
/** Default maximum number of entries. */
35+
public static final int DEFAULT_MAX_ENTRIES = 100;
36+
37+
public static final long DEFAULT_TTL_SECONDS = 600;
38+
39+
private final long ttlMillis;
40+
private final LinkedHashMap<String, CacheEntry<V>> store;
41+
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
42+
43+
/**
44+
* Creates a cache with default settings (100 entries, 10-minute TTL).
45+
*/
46+
public InMemoryAuthCache() {
47+
this(DEFAULT_MAX_ENTRIES, DEFAULT_TTL_SECONDS);
48+
}
49+
50+
/**
51+
* Creates a cache with the specified limits.
52+
*
53+
* @param maxEntries maximum number of entries before LRU eviction
54+
* @param ttlSeconds time-to-live per entry in seconds
55+
*/
56+
public InMemoryAuthCache(int maxEntries, long ttlSeconds) {
57+
if (maxEntries <= 0) {
58+
throw new IllegalArgumentException("maxEntries must be positive");
59+
}
60+
if (ttlSeconds < 0) {
61+
throw new IllegalArgumentException("ttlSeconds must not be negative");
62+
}
63+
this.ttlMillis = ttlSeconds * 1000;
64+
// accessOrder=true makes LinkedHashMap maintain LRU order
65+
this.store = new LinkedHashMap<String, CacheEntry<V>>(maxEntries, 0.75f, true) {
66+
@Override
67+
protected boolean removeEldestEntry(Map.Entry<String, CacheEntry<V>> eldest) {
68+
return size() > maxEntries;
69+
}
70+
};
71+
}
72+
73+
@Override
74+
public V get(String key) {
75+
lock.writeLock().lock();
76+
try {
77+
CacheEntry<V> entry = store.get(key);
78+
if (entry == null) {
79+
return null;
80+
}
81+
if (isExpired(entry)) {
82+
store.remove(key);
83+
return null;
84+
}
85+
return entry.value;
86+
} finally {
87+
lock.writeLock().unlock();
88+
}
89+
}
90+
91+
@Override
92+
public void put(String key, V value) {
93+
lock.writeLock().lock();
94+
try {
95+
store.put(key, new CacheEntry<>(value, System.currentTimeMillis()));
96+
} finally {
97+
lock.writeLock().unlock();
98+
}
99+
}
100+
101+
@Override
102+
public void remove(String key) {
103+
lock.writeLock().lock();
104+
try {
105+
store.remove(key);
106+
} finally {
107+
lock.writeLock().unlock();
108+
}
109+
}
110+
111+
@Override
112+
public void clear() {
113+
lock.writeLock().lock();
114+
try {
115+
store.clear();
116+
} finally {
117+
lock.writeLock().unlock();
118+
}
119+
}
120+
121+
@Override
122+
public int size() {
123+
lock.readLock().lock();
124+
try {
125+
return store.size();
126+
} finally {
127+
lock.readLock().unlock();
128+
}
129+
}
130+
131+
private boolean isExpired(CacheEntry<V> entry) {
132+
if (ttlMillis == 0) {
133+
return false; // TTL of 0 means no expiration
134+
}
135+
return (System.currentTimeMillis() - entry.createdAt) > ttlMillis;
136+
}
137+
138+
/**
139+
* Internal wrapper that pairs a value with its insertion timestamp.
140+
*/
141+
private static final class CacheEntry<V> {
142+
final V value;
143+
final long createdAt;
144+
145+
CacheEntry(V value, long createdAt) {
146+
this.value = value;
147+
this.createdAt = createdAt;
148+
}
149+
}
150+
}

auth0-api-java/src/main/java/com/auth0/examples/Auth0ApiExample.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
package com.auth0.examples;
22

33
import com.auth0.AuthClient;
4+
import com.auth0.DomainResolver;
45
import com.auth0.enums.DPoPMode;
56
import com.auth0.exception.BaseAuthException;
67
import com.auth0.models.AuthOptions;
78
import com.auth0.models.AuthenticationContext;
89
import com.auth0.models.HttpRequestInfo;
10+
import com.auth0.models.RequestContext;
911
import com.sun.net.httpserver.HttpExchange;
1012
import com.sun.net.httpserver.HttpHandler;
1113
import com.sun.net.httpserver.HttpServer;
1214

1315
import java.io.IOException;
1416
import java.io.OutputStream;
1517
import java.net.InetSocketAddress;
18+
import java.util.Arrays;
19+
import java.util.Collections;
1620
import java.util.HashMap;
21+
import java.util.List;
1722
import java.util.Map;
1823

1924
public class Auth0ApiExample {
@@ -88,30 +93,26 @@ public void handle(HttpExchange exchange) throws IOException {
8893

8994
headers.put("authorization", auth);
9095

91-
9296
String dpopHeader = exchange.getRequestHeaders().getFirst("DPoP");
9397

94-
if(dpopHeader != null) {
98+
if (dpopHeader != null) {
9599
headers.put("DPoP", dpopHeader);
96100
}
97101

98-
99102
// Build HttpRequestInfo (needed for DPoP htm + htu validation)
100103
HttpRequestInfo requestInfo = null;
101104
try {
102105
requestInfo = new HttpRequestInfo(
103106
exchange.getRequestMethod(),
104-
"http://localhost:8000" + exchange.getRequestURI().toString(), headers
105-
);
107+
"http://localhost:8000" + exchange.getRequestURI().toString(), headers);
106108
} catch (BaseAuthException e) {
107109
throw new RuntimeException(e);
108110
}
109111

110112
System.out.println("Incoming request to " + requestInfo.toString());
111113

112114
try {
113-
AuthenticationContext claims =
114-
authClient.verifyRequest(requestInfo);
115+
AuthenticationContext claims = authClient.verifyRequest(requestInfo);
115116

116117
String user = (String) claims.getClaims().get("sub");
117118

0 commit comments

Comments
 (0)