Skip to content

Commit 0c4c34e

Browse files
committed
Remove Empty files
1 parent 0652a23 commit 0c4c34e

5 files changed

Lines changed: 241 additions & 11 deletions

File tree

auth0-api-java/src/main/java/com/auth0/JWTValidator.java

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
import com.auth0.jwt.interfaces.DecodedJWT;
1616
import com.auth0.models.HttpRequestInfo;
1717
import com.auth0.models.RequestContext;
18-
import com.auth0.OidcDiscoveryFetcher;
1918

2019
import java.net.MalformedURLException;
2120
import java.net.URL;
2221
import java.security.interfaces.RSAPublicKey;
2322
import java.util.Collections;
2423
import java.util.List;
24+
import java.util.Locale;
2525
import java.util.stream.Collectors;
2626

2727
/**
@@ -138,12 +138,14 @@ public DecodedJWT validateToken(String token, HttpRequestInfo httpRequestInfo) t
138138
throw new VerifyAccessTokenException("Token issuer is not in the allowed list");
139139
}
140140

141-
OidcMetadata discovery = performOidcDiscovery(tokenIss);
141+
OidcMetadata discovery = performOidcDiscovery(normalizedIss);
142142

143-
if (!tokenIss.equals(discovery.getIssuer())) {
143+
if (!normalizedIss.equals(normalizeToUrl(discovery.getIssuer()))) {
144144
throw new VerifyAccessTokenException("Discovery metadata issuer does not match token issuer");
145145
}
146146

147+
validateJwksUriHost(normalizedIss, discovery.getJwksUri());
148+
147149
JwkProvider dynamicJwkProvider = getOrCreateJwkProvider(discovery.getJwksUri());
148150

149151
Jwk jwk = dynamicJwkProvider.get(unverifiedJwt.getKeyId());
@@ -156,6 +158,8 @@ public DecodedJWT validateToken(String token, HttpRequestInfo httpRequestInfo) t
156158

157159
return verifier.verify(token);
158160

161+
} catch (BaseAuthException e) {
162+
throw e;
159163
} catch (Exception e) {
160164
throw new VerifyAccessTokenException("signature verification failed", e);
161165
}
@@ -341,10 +345,37 @@ private List<String> resolveAllowedDomains(String tokenIss, HttpRequestInfo http
341345
return Collections.emptyList();
342346
}
343347

348+
/**
349+
* Validates that the JWKS URI host matches the issuer host.
350+
* <p>
351+
* Prevents a compromised OIDC discovery endpoint from redirecting JWKS
352+
* fetches to an attacker-controlled domain.
353+
* </p>
354+
*
355+
* @param issuerUrl the normalized issuer URL
356+
* @param jwksUri the {@code jwks_uri} from discovery metadata
357+
* @throws VerifyAccessTokenException if the hosts do not match
358+
*/
359+
private void validateJwksUriHost(String issuerUrl, String jwksUri) throws VerifyAccessTokenException {
360+
try {
361+
String issuerHost = new URL(issuerUrl).getHost().toLowerCase(Locale.ROOT);
362+
String jwksHost = new URL(jwksUri).getHost().toLowerCase(Locale.ROOT);
363+
if (!issuerHost.equals(jwksHost)) {
364+
throw new VerifyAccessTokenException("JWKS URI host does not match issuer host");
365+
}
366+
} catch (MalformedURLException e) {
367+
throw new VerifyAccessTokenException("Invalid URL during JWKS URI host validation", e);
368+
}
369+
}
370+
344371
/**
345372
* Normalizes a domain string into a full HTTPS URL with a trailing slash.
346-
* Ensures consistent comparison (e.g., {@code "tenant.auth0.com"} becomes
373+
* <p>
374+
* Lowercases the scheme and host per RFC 3986 (scheme and host are
375+
* case-insensitive). Ensures consistent comparison regardless of how the
376+
* domain was configured (e.g., {@code "Tenant.Auth0.com"} becomes
347377
* {@code "https://tenant.auth0.com/"}).
378+
* </p>
348379
*
349380
* @param domain the raw domain or URL string
350381
* @return the normalized URL, or {@code null} if input is {@code null}
@@ -354,9 +385,28 @@ private String normalizeToUrl(String domain) {
354385
return null;
355386

356387
String url = domain.trim();
357-
if (!url.toLowerCase().startsWith("http")) {
388+
if (!url.toLowerCase(Locale.ROOT).startsWith("http")) {
358389
url = "https://" + url;
359390
}
360-
return url.endsWith("/") ? url : url + "/";
391+
392+
try {
393+
URL parsed = new URL(url);
394+
StringBuilder normalized = new StringBuilder();
395+
normalized.append(parsed.getProtocol().toLowerCase(Locale.ROOT));
396+
normalized.append("://");
397+
normalized.append(parsed.getHost().toLowerCase(Locale.ROOT));
398+
int port = parsed.getPort();
399+
if (port > 0 && port != parsed.getDefaultPort()) {
400+
normalized.append(":").append(port);
401+
}
402+
String path = parsed.getPath();
403+
normalized.append(path == null || path.isEmpty() ? "/" : path);
404+
String result = normalized.toString();
405+
return result.endsWith("/") ? result : result + "/";
406+
} catch (MalformedURLException e) {
407+
// Fallback: simple lowercase normalization
408+
url = url.toLowerCase(Locale.ROOT);
409+
return url.endsWith("/") ? url : url + "/";
410+
}
361411
}
362412
}

auth0-api-java/src/main/java/com/auth0/OidcDiscoveryFetcher.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import org.apache.http.impl.client.HttpClients;
1212
import org.apache.http.util.EntityUtils;
1313

14+
import java.io.Closeable;
1415
import java.io.IOException;
1516

1617
/**
@@ -25,32 +26,44 @@
2526
* <p>
2627
* Thread-safe: delegates thread safety to the {@link AuthCache} implementation.
2728
* </p>
29+
* <p>
30+
* Implements {@link Closeable} to manage the lifecycle of the internal HTTP client.
31+
* Only self-created HTTP clients are closed; externally-provided clients are left
32+
* to the caller.
33+
* </p>
2834
*/
29-
class OidcDiscoveryFetcher {
35+
class OidcDiscoveryFetcher implements Closeable {
3036

3137
static final String CACHE_PREFIX = "discovery:";
3238
private static final String WELL_KNOWN_PATH = ".well-known/openid-configuration";
3339
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
3440

3541
private final AuthCache<Object> cache;
3642
private final CloseableHttpClient httpClient;
43+
private final boolean ownsHttpClient;
3744

3845
/**
3946
* Creates a fetcher with the provided cache and the default HTTP client.
47+
* The fetcher owns the HTTP client and will close it when {@link #close()} is called.
4048
*
4149
* @param cache the unified cache instance
4250
*/
4351
OidcDiscoveryFetcher(AuthCache<Object> cache) {
44-
this(cache, HttpClients.createDefault());
52+
this(cache, HttpClients.createDefault(), true);
4553
}
4654

4755
/**
4856
* Creates a fetcher with the provided cache and a custom HTTP client.
57+
* The caller retains ownership of the HTTP client and is responsible for closing it.
4958
*
5059
* @param cache the unified cache instance
5160
* @param httpClient the HTTP client to use for discovery requests
5261
*/
5362
OidcDiscoveryFetcher(AuthCache<Object> cache, CloseableHttpClient httpClient) {
63+
this(cache, httpClient, false);
64+
}
65+
66+
private OidcDiscoveryFetcher(AuthCache<Object> cache, CloseableHttpClient httpClient, boolean ownsHttpClient) {
5467
if (cache == null) {
5568
throw new IllegalArgumentException("cache must not be null");
5669
}
@@ -59,6 +72,7 @@ class OidcDiscoveryFetcher {
5972
}
6073
this.cache = cache;
6174
this.httpClient = httpClient;
75+
this.ownsHttpClient = ownsHttpClient;
6276
}
6377

6478
/**
@@ -152,4 +166,15 @@ int cacheSize() {
152166
AuthCache<Object> getCache() {
153167
return cache;
154168
}
169+
170+
/**
171+
* Closes the HTTP client if this fetcher owns it.
172+
* Externally-provided HTTP clients are not closed — the caller manages their lifecycle.
173+
*/
174+
@Override
175+
public void close() throws IOException {
176+
if (ownsHttpClient) {
177+
httpClient.close();
178+
}
179+
}
155180
}

auth0-springboot-api/EXAMPLES.md

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,18 +280,164 @@ public class AdminController {
280280
}
281281
```
282282

283+
## Multiple Custom Domains (MCD)
284+
285+
For APIs that accept tokens from multiple Auth0 custom domains (e.g., multi-tenant SaaS, domain migrations).
286+
287+
### 1. Static Domain List
288+
289+
Configure a fixed set of allowed issuer domains in `application.yml`:
290+
291+
```yaml
292+
auth0:
293+
audience: "https://api.example.com"
294+
domains:
295+
- "login.acme.com"
296+
- "auth.partner.com"
297+
- "dev.example.com"
298+
```
299+
300+
Tokens whose `iss` claim matches any of these domains will be accepted. No code changes required.
301+
302+
### 2. Dynamic Domain Resolver
303+
304+
For scenarios where the allowed issuers depend on runtime context (tenant headers, database lookups, etc.), define a `DomainResolver` bean:
305+
306+
```java
307+
import com.auth0.DomainResolver;
308+
import com.auth0.models.RequestContext;
309+
310+
@Configuration
311+
public class McdConfig {
312+
313+
@Bean
314+
public DomainResolver domainResolver(TenantService tenantService) {
315+
return context -> {
316+
// context.getHeaders() — request headers (lowercase keys)
317+
// context.getUrl() — the API request URL
318+
// context.getTokenIssuer() — unverified iss claim (routing hint only)
319+
String tenantId = context.getHeaders().get("x-tenant-id");
320+
List<String> domains = tenantService.getDomainsForTenant(tenantId);
321+
return domains;
322+
};
323+
}
324+
}
325+
```
326+
327+
When a `DomainResolver` bean is present, it takes priority over the static `domains` list. The resolver receives a `RequestContext` with the request URL, headers, and the unverified `iss` claim from the token.
328+
329+
### 3. Domain + Domains Coexistence (Auth for Agents)
330+
331+
For Auth for Agents scenarios, `domain` and `domains` can coexist. The `domain` is used for Auth for Agents flows (token exchange, authorization), while `domains` is used for token validation:
332+
333+
```yaml
334+
auth0:
335+
domain: "primary-tenant.auth0.com" # For Auth for Agents flows
336+
audience: "https://api.example.com"
337+
domains: # For token validation
338+
- "primary-tenant.auth0.com"
339+
- "tenant2.auth0.com"
340+
- "tenant3.auth0.com"
341+
```
342+
343+
When both are present, the SDK always uses `domains` for token verification.
344+
345+
## Caching
346+
347+
The SDK caches OIDC discovery metadata and JWKS providers in a unified cache. By default, it uses a thread-safe in-memory LRU cache.
348+
349+
### Cache Configuration
350+
351+
```yaml
352+
auth0:
353+
domain: "your-tenant.auth0.com"
354+
audience: "https://api.example.com"
355+
cache-max-entries: 100 # Max entries before LRU eviction (default: 100)
356+
cache-ttl-seconds: 600 # TTL per entry in seconds (default: 600 = 10 minutes)
357+
```
358+
359+
Both OIDC discovery and JWKS entries count against the `cache-max-entries` limit.
360+
361+
### Custom Cache Implementation
362+
363+
Replace the default in-memory cache with a distributed backend (Redis, Memcached, etc.) by implementing the `AuthCache` interface and registering it as a Spring bean:
364+
365+
```java
366+
import com.auth0.AuthCache;
367+
368+
public class RedisAuthCache implements AuthCache<Object> {
369+
370+
private final RedisTemplate<String, Object> redisTemplate;
371+
private final Duration ttl;
372+
373+
public RedisAuthCache(RedisTemplate<String, Object> redisTemplate, Duration ttl) {
374+
this.redisTemplate = redisTemplate;
375+
this.ttl = ttl;
376+
}
377+
378+
@Override
379+
public Object get(String key) {
380+
return redisTemplate.opsForValue().get(key);
381+
}
382+
383+
@Override
384+
public void put(String key, Object value) {
385+
redisTemplate.opsForValue().set(key, value, ttl);
386+
}
387+
388+
@Override
389+
public void remove(String key) {
390+
redisTemplate.delete(key);
391+
}
392+
393+
@Override
394+
public void clear() {
395+
Set<String> keys = redisTemplate.keys("discovery:*");
396+
if (keys != null) redisTemplate.delete(keys);
397+
keys = redisTemplate.keys("jwks:*");
398+
if (keys != null) redisTemplate.delete(keys);
399+
}
400+
401+
@Override
402+
public int size() {
403+
return 0; // approximate
404+
}
405+
}
406+
```
407+
408+
Register it as a bean — the auto-configuration picks it up automatically:
409+
410+
```java
411+
@Configuration
412+
public class CacheConfig {
413+
414+
@Bean
415+
public AuthCache<Object> authCache(RedisTemplate<String, Object> redisTemplate) {
416+
return new RedisAuthCache(redisTemplate, Duration.ofMinutes(10));
417+
}
418+
}
419+
```
420+
421+
When a custom `AuthCache` bean is present, the `cache-max-entries` and `cache-ttl-seconds` properties are ignored — your implementation controls its own eviction and TTL.
422+
283423
## Configuration Reference
284424

285425
### Complete Configuration Example
286426

287427
```yaml
288428
auth0:
289-
# Required: Your Auth0 domain
429+
# Required (unless domains or domainsResolver is set): Your Auth0 domain
290430
domain: "your-tenant.auth0.com"
291431
292432
# Required: API identifier/audience
293433
audience: "https://api.example.com"
294434
435+
# Optional: Static list of allowed issuer domains (MCD)
436+
# Mutually exclusive with domainsResolver bean
437+
domains:
438+
- "login.acme.com"
439+
- "auth.partner.com"
440+
295441
# Optional: DPoP mode (DISABLED, ALLOWED, REQUIRED)
296442
# Default: ALLOWED
297443
dpop-mode: ALLOWED
@@ -303,6 +449,14 @@ auth0:
303449
# Optional: DPoP proof time leeway in seconds
304450
# Default: 30 (30 seconds)
305451
dpop-iat-leeway-seconds: 30
452+
453+
# Optional: Max cache entries before LRU eviction
454+
# Default: 100
455+
cache-max-entries: 100
456+
457+
# Optional: Cache TTL in seconds
458+
# Default: 600 (10 minutes)
459+
cache-ttl-seconds: 600
306460
```
307461

308462
### Environment Variables
@@ -312,9 +466,12 @@ You can also configure using environment variables:
312466
```bash
313467
AUTH0_DOMAIN=your-tenant.auth0.com
314468
AUTH0_AUDIENCE=https://api.example.com
469+
AUTH0_DOMAINS=login.acme.com,auth.partner.com
315470
AUTH0_DPOPMODE=ALLOWED
316471
AUTH0_DPOPIATOFFSETSECONDS=300
317472
AUTH0_DPOPIATLEEWAYSSECONDS=30
473+
AUTH0_CACHEMAXENTRIES=100
474+
AUTH0_CACHETTLSECONDS=600
318475
```
319476

320477
> **Note:** Spring Boot environment variable binding removes dashes and is case-insensitive. Do not use underscores to separate words within a property name (e.g., use `AUTH0_DPOPMODE`, not `AUTH0_DPOP_MODE`).

auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0DomainResolver.java

Lines changed: 0 additions & 1 deletion
This file was deleted.

auth0-springboot-api/src/main/java/com/auth0/spring/boot/Auth0RequestContext.java

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)