Auth0 Spring Boot API Examples
April 8, 2026 · View on GitHub
This document provides examples for using the auth0-springboot-api package to validate Auth0 tokens in your Spring Boot applications.
Basic Configuration
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.com
Bearer Authentication
Bearer authentication is the standard OAuth 2.0 token authentication method.
Basic Setup
@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();
}
}
Protected Endpoint Example
@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"
));
}
}
Accessing Token Claims
@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 Authentication
DPoP (Demonstrating Proof of Possession) is an application-level mechanism for sender-constraining OAuth 2.0 access and refresh tokens.
Configuration Modes
1. Allowed Mode (Default)
Accepts both Bearer and DPoP tokens:
auth0:
domain: "your-tenant.auth0.com"
audience: "https://api.example.com"
dpop-mode: ALLOWED
2. Required Mode
Only accepts DPoP tokens:
auth0:
domain: "your-tenant.auth0.com"
audience: "https://api.example.com"
dpop-mode: REQUIRED
3. Disabled Mode
Only accepts Bearer tokens:
auth0:
domain: "your-tenant.auth0.com"
audience: "https://api.example.com"
dpop-mode: DISABLED
Advanced DPoP Configuration
auth0:
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)
How DPoP Works in Your Controllers
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
Scope-Based Authorization
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.
Option 1: Security Filter Chain (Recommended)
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();
}
}
Option 2: Method-Level Security with @PreAuthorize
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();
}
}
Option 3: Programmatic Scope Check
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)
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.
1. Static Domain List
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.
2. Dynamic Domain Resolver
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.
Caching
The SDK caches OIDC discovery metadata and JWKS providers in a unified cache. By default, it uses a thread-safe in-memory LRU cache.
Cache Configuration
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.
Custom Cache Implementation
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.
Configuration Reference
Complete Configuration Example
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: 600
Environment Variables
You 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=600
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, notAUTH0_DPOP_MODE).
Error Handling
Common HTTP Status Codes
- 401 Unauthorized: Missing or invalid token
- 403 Forbidden: Valid token but insufficient permissions
WWW-Authenticate Headers
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"