This document provides examples for using the auth0-springboot-api package to validate Auth0 tokens in your Spring Boot applications.
Configure your Auth0 settings in application.yml:
auth0:
domain: "your-tenant.auth0.com"
audience: "https://api.example.com"Or in application.properties:
auth0.domain=your-tenant.auth0.com
auth0.audience=https://api.example.comBearer authentication is the standard OAuth 2.0 token authentication method.
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain apiSecurity(
HttpSecurity http,
Auth0AuthenticationFilter authFilter
) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public").permitAll()
.requestMatchers("/api/protected").authenticated()
.anyRequest().authenticated()
)
.addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/protected")
public ResponseEntity<Map<String, Object>> protectedEndpoint(Authentication authentication) {
String userId = authentication.getName(); // Returns the 'sub' claim
return ResponseEntity.ok(Map.of(
"message", "Access granted!",
"user", userId,
"authenticated", true
));
}
@GetMapping("/public")
public ResponseEntity<Map<String, Object>> publicEndpoint() {
return ResponseEntity.ok(Map.of(
"message", "Public endpoint - no token required"
));
}
}@RestController
@RequestMapping("/api")
public class UserController {
@GetMapping("/profile")
public ResponseEntity<Map<String, Object>> getUserProfile(Authentication authentication) {
if (authentication instanceof Auth0AuthenticationToken auth0Token) {
return ResponseEntity.ok(Map.of(
"sub", String.valueOf(auth0Token.getClaim("sub")),
"email", String.valueOf(auth0Token.getClaim("email")),
"scope", String.valueOf(auth0Token.getClaim("scope")),
"scopes", auth0Token.getScopes()
));
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}DPoP (Demonstrating Proof of Possession) is an application-level mechanism for sender-constraining OAuth 2.0 access and refresh tokens.
Accepts both Bearer and DPoP tokens:
auth0:
domain: "your-tenant.auth0.com"
audience: "https://api.example.com"
dpop-mode: ALLOWEDOnly accepts DPoP tokens:
auth0:
domain: "your-tenant.auth0.com"
audience: "https://api.example.com"
dpop-mode: REQUIREDOnly accepts Bearer tokens:
auth0:
domain: "your-tenant.auth0.com"
audience: "https://api.example.com"
dpop-mode: DISABLEDauth0:
domain: "your-tenant.auth0.com"
audience: "https://api.example.com"
dpop-mode: ALLOWED
dpop-iat-offset-seconds: 300 # DPoP proof time window (default: 300)
dpop-iat-leeway-seconds: 30 # DPoP proof time leeway (default: 30)DPoP validation is handled entirely by the library at the filter level. Your controllers don't need any DPoP-specific code — the library validates the DPoP proof automatically before the request reaches your controller. A validated DPoP request produces the same Auth0AuthenticationToken as a Bearer request:
@RestController
@RequestMapping("/api")
public class SensitiveDataController {
@GetMapping("/sensitive")
public ResponseEntity<Map<String, Object>> sensitiveEndpoint(Authentication authentication) {
// This works the same whether the client used Bearer or DPoP.
// DPoP proof validation already happened in the filter.
if (authentication instanceof Auth0AuthenticationToken auth0Token) {
return ResponseEntity.ok(Map.of(
"user", authentication.getName(),
"scopes", auth0Token.getScopes(),
"message", "Access granted"
));
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}The difference is in what the library rejects:
ALLOWEDmode: Accepts bothAuthorization: Bearer <token>andAuthorization: DPoP <token>+DPoP: <proof>REQUIREDmode: Rejects Bearer tokens — onlyDPoPtokens with a valid proof are acceptedDISABLEDmode: Rejects DPoP tokens — onlyBearertokens are accepted
The library maps JWT scopes to Spring Security authorities with a SCOPE_ prefix. For example, a token with scope: "read:messages write:messages" produces authorities SCOPE_read:messages and SCOPE_write:messages.
The simplest approach — define scope requirements in your security configuration:
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain apiSecurity(
HttpSecurity http,
Auth0AuthenticationFilter authFilter
) throws Exception {
return http
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public").permitAll()
.requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin")
.requestMatchers("/api/users/**").hasAuthority("SCOPE_read:users")
.anyRequest().authenticated()
)
.addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}For fine-grained control per method. Requires @EnableMethodSecurity on a configuration class:
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
// Enables @PreAuthorize annotations
}@RestController
@RequestMapping("/api/users")
public class UserManagementController {
@GetMapping
@PreAuthorize("hasAuthority('SCOPE_read:users')")
public ResponseEntity<List<User>> getUsers() {
return ResponseEntity.ok(userService.getAllUsers());
}
@PostMapping
@PreAuthorize("hasAuthority('SCOPE_write:users')")
public ResponseEntity<User> createUser(@RequestBody User user) {
return ResponseEntity.ok(userService.createUser(user));
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('SCOPE_delete:users')")
public ResponseEntity<Void> deleteUser(@PathVariable String id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}Use getScopes() on the token directly when you need custom logic:
@RestController
@RequestMapping("/api")
public class AdminController {
@GetMapping("/admin")
public ResponseEntity<Map<String, Object>> adminEndpoint(Authentication authentication) {
if (authentication instanceof Auth0AuthenticationToken auth0Token) {
Set<String> scopes = auth0Token.getScopes();
if (!scopes.contains("admin") || !scopes.contains("read:admin")) {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Map.of("error", "insufficient_scope"));
}
return ResponseEntity.ok(Map.of("message", "Admin access granted"));
}
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}Multiple Custom Domains (MCD) support enables a single API application to accept access tokens issued by multiple domains associated with the same Auth0 tenant, including the canonical domain and its custom domains.
This is commonly required in scenarios such as:
- Multi-brand applications (B2C) where each brand uses a different custom domain but they all share the same API.
- A single API serves multiple frontend applications that use different custom domains.
- A gradual migration from the canonical domain to a custom domain, where both domains need to be supported during the transition period.
In these cases, your API must trust and validate tokens from multiple issuers instead of a single domain. The SDK supports two approaches for configuring multiple domains, Static Domain List and Dynamic Domain Resolver.
Configure a fixed set of allowed issuer domains in application.yml:
auth0:
audience: "https://api.example.com"
domains:
- "brandA.acme.com"
- "brandB.acme.com"
- "brandC.acme.com"Tokens whose iss claim matches any of these domains will be accepted. No code changes required.
For scenarios where the allowed issuers depend on runtime context (tenant headers, database lookups, etc.), define a DomainResolver bean:
import com.auth0.DomainResolver;
import com.auth0.models.RequestContext;
@Configuration
public class McdConfig {
@Bean
public DomainResolver domainResolver(TenantService tenantService) {
return context -> {
// context.getHeaders() — request headers (lowercase keys)
// context.getUrl() — the API request URL
// context.getTokenIssuer() — unverified iss claim (routing hint only)
String tenantId = context.getHeaders().get("x-tenant-id");
List<String> domains = tenantService.getDomainsForTenant(tenantId);
return domains;
};
}
}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.
The SDK caches OIDC discovery metadata and JWKS providers in a unified cache. By default, it uses a thread-safe in-memory LRU cache.
auth0:
domain: "your-tenant.auth0.com"
audience: "https://api.example.com"
cache-max-entries: 100 # Max entries before LRU eviction (default: 100)
cache-ttl-seconds: 600 # TTL per entry in seconds (default: 600 = 10 minutes)Both OIDC discovery and JWKS entries count against the cache-max-entries limit.
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:
import com.auth0.AuthCache;
public class RedisAuthCache implements AuthCache<Object> {
private final RedisTemplate<String, Object> redisTemplate;
private final Duration ttl;
public RedisAuthCache(RedisTemplate<String, Object> redisTemplate, Duration ttl) {
this.redisTemplate = redisTemplate;
this.ttl = ttl;
}
@Override
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
@Override
public void put(String key, Object value) {
redisTemplate.opsForValue().set(key, value, ttl);
}
@Override
public void remove(String key) {
redisTemplate.delete(key);
}
@Override
public void clear() {
Set<String> keys = redisTemplate.keys("discovery:*");
if (keys != null) redisTemplate.delete(keys);
keys = redisTemplate.keys("jwks:*");
if (keys != null) redisTemplate.delete(keys);
}
@Override
public int size() {
return 0; // approximate
}
}Register it as a bean — the auto-configuration picks it up automatically:
@Configuration
public class CacheConfig {
@Bean
public AuthCache<Object> authCache(RedisTemplate<String, Object> redisTemplate) {
return new RedisAuthCache(redisTemplate, Duration.ofMinutes(10));
}
}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.
auth0:
# Required (unless domains or domainsResolver is set): Your Auth0 domain
domain: "your-tenant.auth0.com"
# Required: API identifier/audience
audience: "https://api.example.com"
# Optional: Static list of allowed issuer domains (MCD)
# Mutually exclusive with domainsResolver bean
domains:
- "login.acme.com"
- "auth.partner.com"
# Optional: DPoP mode (DISABLED, ALLOWED, REQUIRED)
# Default: ALLOWED
dpop-mode: ALLOWED
# Optional: DPoP proof time window in seconds
# Default: 300 (5 minutes)
dpop-iat-offset-seconds: 300
# Optional: DPoP proof time leeway in seconds
# Default: 30 (30 seconds)
dpop-iat-leeway-seconds: 30
# Optional: Max cache entries before LRU eviction
# Default: 100
cache-max-entries: 100
# Optional: Cache TTL in seconds
# Default: 600 (10 minutes)
cache-ttl-seconds: 600You can also configure using environment variables:
AUTH0_DOMAIN=your-tenant.auth0.com
AUTH0_AUDIENCE=https://api.example.com
AUTH0_DOMAINS=login.acme.com,auth.partner.com
AUTH0_DPOPMODE=ALLOWED
AUTH0_DPOPIATOFFSETSECONDS=300
AUTH0_DPOPIATLEEWAYSSECONDS=30
AUTH0_CACHEMAXENTRIES=100
AUTH0_CACHETTLSECONDS=600Note: 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, notAUTH0_DPOP_MODE).
- 401 Unauthorized: Missing or invalid token
- 403 Forbidden: Valid token but insufficient permissions
The library automatically sets appropriate WWW-Authenticate headers on authentication failures:
# ALLOWED mode (default)
WWW-Authenticate: Bearer realm="api", DPoP algs="ES256"
# REQUIRED mode
WWW-Authenticate: DPoP algs="ES256"
# DPoP-specific errors
WWW-Authenticate: DPoP error="invalid_dpop_proof", error_description="DPoP proof validation failed"