← Files Auth0ARCHIVED FILE
skills/auth0/references/framework-springboot-api/index.md
37.7 KB · Sep 30, 2026 · 23:15 UTC
# Auth0 Spring Boot API Integration
Protect Spring Boot API endpoints with JWT access token validation using `com.auth0:auth0-springboot-api`. Features auto-configuration, scope-based authorization, and built-in DPoP (RFC 9449) support.
## Critical rules
- **DPoP `dpop-mode`:** `ALLOWED` accepts both Bearer and DPoP tokens; `REQUIRED` accepts only DPoP tokens and rejects standard Bearer; `DISABLED` accepts standard Bearer only. Pick the mode deliberately — `REQUIRED` will 401 any plain Bearer request. See the DPoP Authentication section for full config.
> **Agent instruction:** Before providing SDK setup instructions, fetch the latest release version by running:
> ```bash
> gh api repos/auth0/auth0-auth-java/releases/latest --jq '.tag_name'
> ```
> Use the returned version in all dependency lines instead of any hardcoded version below. If the API call fails (e.g., no releases yet), use `1.0.0-beta.1`.
## Prerequisites
- Java 17+ and Spring Boot 3.2+
- Maven 3.6+ or Gradle 7+
- Auth0 API configured (not Application — must be API resource)
- If Auth0 isn't set up yet, set it up first with the Auth0 CLI (`auth0 login`, then `auth0 apps create`)
## When NOT to Use
| Use Case | Use Instead |
|----------|------------------|
| Server-rendered web applications (Spring MVC with sessions) | Use the Auth0 integration workflow for Spring Boot web apps with login UI (Regular Web Application) |
| Single Page Applications | Use the Auth0 integration workflow for React, Vue, or Angular for client-side auth |
| Mobile applications | Use the Auth0 integration workflow for Android or iOS/Swift for native mobile |
| Non-Spring Java APIs | Use the Auth0 integration workflow for plain Spring Security |
## Quick Start Workflow
> **Agent instruction:** If the user's prompt already provides Auth0 credentials (domain, audience), use them directly — skip automatic setup and credential questions. Only offer setup options when credentials are missing.
### 1. Install SDK
**Gradle (build.gradle):**
```groovy
implementation 'com.auth0:auth0-springboot-api:1.0.0-beta.1'
```
**Maven (pom.xml):**
```xml
<dependency>
<groupId>com.auth0</groupId>
<artifactId>auth0-springboot-api</artifactId>
<version>1.0.0-beta.1</version>
</dependency>
```
### 2. Create Auth0 API
You need an **API** (not Application) in Auth0.
> **STOP — ask the user before proceeding.**
>
> Ask exactly this question and wait for their answer before doing anything else:
>
> > "How would you like to create the Auth0 API resource?
> > 1. **Automated** — I'll run Auth0 CLI scripts that create the resource and write the values to your application.yml automatically.
> > 2. **Manual** — You create the API yourself in the Auth0 Dashboard (or via `auth0 apis create`) and provide me the Domain and Audience.
> >
> > Which do you prefer? (1 = Automated / 2 = Manual)"
>
> Do NOT proceed to any setup steps until the user has answered. Do NOT default to manual.
**If the user chose Automated**, follow the Setup Guide section below for complete CLI scripts. The automated path writes `application.yml` for you — skip Step 3 below and proceed directly to Step 4.
**If the user chose Manual**, follow the Setup Guide section below (Manual Setup). Then continue with Step 3.
Quick reference for manual API creation:
```bash
# Using Auth0 CLI
auth0 apis create \
--name "My Spring Boot API" \
--identifier https://my-springboot-api
```
Or create manually in Auth0 Dashboard → Applications → APIs
### 3. Configure application.yml
```yaml
auth0:
domain: "your-tenant.auth0.com"
audience: "https://my-springboot-api"
```
**Important:** Domain must NOT include `https://`. The library constructs the issuer URL automatically.
Or use `application.properties`:
```properties
auth0.domain=your-tenant.auth0.com
auth0.audience=https://my-springboot-api
```
### 4. Configure Spring Security
```java
@Configuration
@EnableMethodSecurity
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()
.requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin")
.anyRequest().authenticated())
.addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}
```
### 5. Protect Endpoints
```java
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/public")
public ResponseEntity<Map<String, Object>> publicEndpoint() {
return ResponseEntity.ok(Map.of("message", "Public endpoint - no token required"));
}
@GetMapping("/protected")
public ResponseEntity<Map<String, Object>> protectedEndpoint(Authentication authentication) {
Auth0AuthenticationToken token = (Auth0AuthenticationToken) authentication;
return ResponseEntity.ok(Map.of(
"user", authentication.getName(),
"email", token.getClaim("email"),
"scopes", token.getScopes()
));
}
}
```
### 6. Test API
> **Agent instruction:** After writing all code, verify the build succeeds:
> ```bash
> ./gradlew bootRun
> ```
> or `./mvnw spring-boot:run`. If build fails, diagnose and fix. After 5-6 failed attempts, use `AskUserQuestion` to get help.
Test public endpoint:
```bash
curl http://localhost:8080/api/public
```
Test protected endpoint (requires access token):
```bash
curl http://localhost:8080/api/protected \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```
Get a test token via Client Credentials flow or Auth0 Dashboard → APIs → Test tab.
## Common Mistakes
| Mistake | Fix |
|---------|-----|
| Domain includes `https://` | Use `your-tenant.auth0.com` format only — no scheme prefix |
| Audience doesn't match API Identifier | Must exactly match the API Identifier set in Auth0 Dashboard |
| Created Application instead of API in Auth0 | Must create API resource in Auth0 Dashboard → Applications → APIs |
| Missing `addFilterBefore` in SecurityConfig | `Auth0AuthenticationFilter` must be added before `UsernamePasswordAuthenticationFilter` |
| Using ID token instead of access token | Must use **access token** for API auth, not ID token |
| Checking `scope` claim in wrong format | Scopes map to `SCOPE_` prefixed authorities: use `hasAuthority("SCOPE_read:data")` |
| Spring Boot env var binding | Use `AUTH0_DOMAIN` not `AUTH0_DOMAIN` with underscores inside property names; Spring removes dashes and is case-insensitive |
## Scope-Based Authorization
See the Integration Guide section below for defining and enforcing scope-based access control via filter chain, `@PreAuthorize`, or programmatic checks.
## DPoP Support
Built-in proof-of-possession token binding per RFC 9449. See the Integration Guide section below for configuration modes (DISABLED, ALLOWED, REQUIRED).
## Related Capabilities
- Basic Auth0 setup and account creation → set it up with the Auth0 CLI (`auth0 login`, then `auth0 apps create`)
- Spring Boot web apps with login UI (Regular Web Application) → the Auth0 integration workflow for Spring Boot web apps
## Quick Reference
**Configuration Properties (`application.yml`):**
- `auth0.domain` — Auth0 tenant domain, no `https://` prefix (required)
- `auth0.audience` — API Identifier from Auth0 API settings (required)
- `auth0.dpop-mode` — DPoP mode: `DISABLED`, `ALLOWED` (default), `REQUIRED`
- `auth0.dpop-iat-offset-seconds` — DPoP proof time window (default: 300)
- `auth0.dpop-iat-leeway-seconds` — DPoP proof time leeway (default: 30)
**User Claims (via `Auth0AuthenticationToken`):**
- `authentication.getName()` — User ID (subject / `sub` claim)
- `token.getClaim("email")` — Any specific claim by name
- `token.getClaims()` — All JWT claims as `Map<String, Object>`
- `token.getScopes()` — Scopes as `Set<String>`
**Common Use Cases:**
- Protect routes → `requestMatchers("/path").authenticated()` (see Step 4)
- Scope enforcement → `hasAuthority("SCOPE_read:data")` or `@PreAuthorize` (see the Integration Guide section below)
- DPoP token binding → see the Integration Guide section below
- Complete API reference → see the API Reference section below
## References
- [Auth0 Java Spring Security API Quickstart](https://auth0.com/docs/quickstart/backend/java-spring-security5)
- [SDK GitHub Repository](https://github.com/auth0/auth0-auth-java)
- [Spring Security Documentation](https://docs.spring.io/spring-security/reference/)
- [Access Tokens Guide](https://auth0.com/docs/secure/tokens/access-tokens)
- [DPoP RFC 9449](https://datatracker.ietf.org/doc/html/rfc9449)
---
# API Reference & Testing
Complete reference for `com.auth0:auth0-springboot-api` configuration options and auto-configuration classes.
---
## Configuration Reference
### application.yml Properties
```yaml
auth0:
domain: "your-tenant.auth0.com" # Required: Auth0 tenant domain (no https://)
audience: "https://api.example.com" # Required: API identifier / audience
dpop-mode: ALLOWED # Optional: DISABLED | ALLOWED | REQUIRED (default: ALLOWED)
dpop-iat-offset-seconds: 300 # Optional: DPoP proof time window (default: 300)
dpop-iat-leeway-seconds: 30 # Optional: DPoP proof time leeway (default: 30)
```
### application.properties Equivalent
```properties
auth0.domain=your-tenant.auth0.com
auth0.audience=https://api.example.com
auth0.dpopMode=ALLOWED
auth0.dpopIatOffsetSeconds=300
auth0.dpopIatLeewaySeconds=30
```
### Environment Variables
```bash
AUTH0_DOMAIN=your-tenant.auth0.com
AUTH0_AUDIENCE=https://api.example.com
AUTH0_DPOPMODE=ALLOWED
AUTH0_DPOPIATOFFSETSECONDS=300
AUTH0_DPOPIATLEEWAYSECONDS=30
```
> **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`).
---
## Auth0Properties
| Property | Type | Required | Default | Description |
|----------|------|----------|---------|-------------|
| `domain` | `String` | Yes | — | Auth0 tenant domain. Format: `your-tenant.auth0.com` (no `https://` prefix) |
| `audience` | `String` | Yes | — | API Identifier from Auth0 Dashboard |
| `dpopMode` | `DPoPMode` | No | `ALLOWED` | Controls which token types are accepted |
| `dpopIatOffsetSeconds` | `Long` | No | `300` | Maximum age of DPoP proof `iat` claim in seconds |
| `dpopIatLeewaySeconds` | `Long` | No | `30` | Additional leeway for DPoP proof time validation |
| `domains` | `List<String>` | No | — | Additional trusted Auth0 domains (for Multiple Custom Domains) |
| `cacheMaxEntries` | `Integer` | No | — | Maximum entries in the JWKS cache |
| `cacheTtlSeconds` | `Long` | No | — | TTL in seconds for JWKS cache entries |
### Auto-Configuration Beans
The SDK auto-configuration also supports custom beans:
| Bean | Description |
|------|-------------|
| `DomainResolver` | Custom domain resolution for Multiple Custom Domains (MCD). Provide a `@Bean` of type `DomainResolver` to route requests to different Auth0 domains based on the request. |
| `AuthCache` | Custom cache implementation for JWKS or token verification results. Provide a `@Bean` of type `AuthCache` to override the default in-memory cache. |
### DPoPMode Enum
| Value | Description |
|-------|-------------|
| `DPoPMode.DISABLED` | Standard JWT Bearer only — rejects DPoP tokens |
| `DPoPMode.ALLOWED` | Accept both DPoP-bound and standard Bearer tokens (default) |
| `DPoPMode.REQUIRED` | Only accept DPoP-bound tokens — rejects standard Bearer |
---
## Auto-Configuration Classes
### Auth0AutoConfiguration
Automatically creates `AuthOptions` and `AuthClient` beans from `Auth0Properties`.
```java
// AuthOptions bean — built from application.yml
@Bean
public AuthOptions authOptions(Auth0Properties properties) {
AuthOptions.Builder builder = new AuthOptions.Builder()
.domain(properties.getDomain())
.audience(properties.getAudience());
if (properties.getDpopMode() != null) {
builder.dpopMode(properties.getDpopMode());
}
if (properties.getDpopIatLeewaySeconds() != null) {
builder.dpopIatLeewaySeconds(properties.getDpopIatLeewaySeconds());
}
if (properties.getDpopIatOffsetSeconds() != null) {
builder.dpopIatOffsetSeconds(properties.getDpopIatOffsetSeconds());
}
return builder.build();
}
// AuthClient bean — main entry point for verifying HTTP requests
@Bean
@ConditionalOnMissingBean
public AuthClient authClient(AuthOptions options) {
return AuthClient.from(options);
}
```
### Auth0SecurityAutoConfiguration
Automatically creates the `Auth0AuthenticationFilter` bean.
```java
@Bean
@ConditionalOnMissingBean
public Auth0AuthenticationFilter authAuthenticationFilter(
AuthClient authClient, Auth0Properties auth0Properties) {
return new Auth0AuthenticationFilter(authClient, auth0Properties);
}
```
### Auth0AuthenticationFilter
A `OncePerRequestFilter` that:
1. Extracts the `Authorization` header
2. Calls `AuthClient.verifyRequest()` to validate the JWT (and DPoP proof if present)
3. Sets `Auth0AuthenticationToken` in the `SecurityContextHolder`
4. On failure, returns appropriate HTTP status and `WWW-Authenticate` header
---
## Auth0AuthenticationToken
Extends `AbstractAuthenticationToken`. Created after successful JWT validation.
| Method | Return Type | Description |
|--------|-------------|-------------|
| `getName()` | `String` | User ID (`sub` claim from JWT) |
| `getClaims()` | `Map<String, Object>` | All JWT claims |
| `getClaim(String claimName)` | `Object` | Specific claim value, or `null` |
| `getScopes()` | `Set<String>` | Parsed scopes from `scope` claim |
| `getAuthorities()` | `Collection<GrantedAuthority>` | `SCOPE_` prefixed authorities from JWT scopes |
**Authority mapping:** The `scope` claim `"read:data write:data"` becomes authorities `SCOPE_read:data` and `SCOPE_write:data`. If no scopes are present, a default `ROLE_USER` authority is assigned.
---
## Claims Reference
### Standard JWT Claims
| Claim | Description | Access |
|-------|-------------|--------|
| `sub` | User ID (subject) | `authentication.getName()` or `token.getClaim("sub")` |
| `scope` | Space-separated scopes | `token.getScopes()` or `token.getClaim("scope")` |
| `aud` | Audience (API identifier) | `token.getClaim("aud")` |
| `iss` | Issuer (Auth0 tenant URL) | `token.getClaim("iss")` |
| `exp` | Expiration timestamp | `token.getClaim("exp")` |
| `iat` | Issued-at timestamp | `token.getClaim("iat")` |
### Auth0-Specific Claims
| Claim | Description |
|-------|-------------|
| `permissions` | Array of RBAC permissions (if Enable RBAC is on) |
| `email` | User email (if requested in scope) |
| `https://example.com/*` | Custom claims added via Auth0 Actions (namespaced) |
---
## Complete Minimal Example
```java
// src/main/java/com/example/SecurityConfig.java
@Configuration
@EnableMethodSecurity
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")
.anyRequest().authenticated())
.addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
}
// src/main/java/com/example/ApiController.java
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/public")
public ResponseEntity<Map<String, Object>> publicEndpoint() {
return ResponseEntity.ok(Map.of("message", "Public endpoint"));
}
@GetMapping("/protected")
public ResponseEntity<Map<String, Object>> protectedEndpoint(Authentication authentication) {
Auth0AuthenticationToken token = (Auth0AuthenticationToken) authentication;
return ResponseEntity.ok(Map.of(
"user", authentication.getName(),
"scopes", token.getScopes()
));
}
@GetMapping("/admin/dashboard")
public ResponseEntity<Map<String, Object>> adminEndpoint(Authentication authentication) {
return ResponseEntity.ok(Map.of(
"message", "Admin access granted",
"user", authentication.getName()
));
}
}
```
```yaml
# src/main/resources/application.yml
auth0:
domain: "your-tenant.auth0.com"
audience: "https://my-springboot-api"
spring:
application:
name: auth0-api
```
---
## Testing Checklist
1. **Public endpoint returns 200 without token:**
```bash
curl http://localhost:8080/api/public
```
2. **Protected endpoint returns 401 without token:**
```bash
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/protected
# Expected: 401
```
3. **Protected endpoint returns 200 with valid token:**
```bash
curl http://localhost:8080/api/protected \
-H "Authorization: Bearer $TOKEN"
```
Capture the token into a shell variable and reference `$TOKEN` rather than
pasting the raw token inline — inline token values leak into shell history
and terminal scrollback. See [Testing with curl](#testing-with-curl) below.
4. **Scope-protected endpoint returns 403 with insufficient scope:**
```bash
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/admin/dashboard \
-H "Authorization: Bearer TOKEN_WITHOUT_ADMIN_SCOPE"
# Expected: 403
```
5. **DPoP token accepted (if dpop-mode is `ALLOWED` or `REQUIRED`):**
```bash
curl http://localhost:8080/api/protected \
-H "Authorization: DPoP YOUR_DPOP_TOKEN" \
-H "DPoP: YOUR_DPOP_PROOF"
```
---
## Common Issues
| Issue | Cause | Fix |
|-------|-------|-----|
| 401 `invalid_token` | Audience mismatch | Verify `auth0.audience` matches API Identifier exactly |
| 401 `invalid_issuer` | Domain has `https://` prefix | Use `your-tenant.auth0.com` format only |
| 403 Forbidden | Token missing required scope | Request token with correct scopes; check `hasAuthority` values |
| No `Auth0AuthenticationFilter` bean | Missing auto-configuration | Ensure `auth0-springboot-api` is on classpath and `auth0.domain`/`auth0.audience` are set |
| DPoP `invalid_dpop_proof` | Proof validation failed | Check DPoP proof format, `iat` claim within time window |
| Token expired | Short-lived test token | Request a fresh token from Auth0 Dashboard or CLI |
| Multiple Authorization headers | Duplicate header sent | Send exactly one `Authorization` header per request |
---
## Security Considerations
- **No client secret needed** — This library validates JWTs via JWKS (public key), not client credentials
- **Never hardcode domain or audience** — Use `application.yml` or environment variables
- **Use HTTPS in production** — Auth0 requires HTTPS for token issuance; API should also use HTTPS
- **Stateless sessions** — Always configure `SessionCreationPolicy.STATELESS` for API endpoints
- **Use minimal scopes** — Only enforce scopes your API actually needs
- **Keep packages updated** — Regularly update `auth0-springboot-api` for security patches
- **DPoP for high-security APIs** — Enable `dpop-mode: REQUIRED` to prevent token theft
---
## References
- [Auth0 Java Spring Security API Quickstart](https://auth0.com/docs/quickstart/backend/java-spring-security5)
- [SDK GitHub Repository](https://github.com/auth0/auth0-auth-java)
- [Spring Security Documentation](https://docs.spring.io/spring-security/reference/)
- [DPoP RFC 9449](https://datatracker.ietf.org/doc/html/rfc9449)
---
# Auth0 Spring Boot API Integration Patterns
Advanced integration patterns for Spring Boot API applications using `auth0-springboot-api`.
---
## Scope-Based Authorization
The library maps JWT scopes to Spring Security authorities with a `SCOPE_` prefix. A token with `scope: "read:messages write:messages"` produces authorities `SCOPE_read:messages` and `SCOPE_write:messages`.
### Option 1: Security Filter Chain (Recommended)
Define scope requirements in your security configuration:
```java
@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
Requires `@EnableMethodSecurity` on a configuration class:
```java
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
// Enables @PreAuthorize annotations
}
```
```java
@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 for custom logic:
```java
@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();
}
```
### Define Permissions in Auth0
1. Go to Auth0 Dashboard → Applications → APIs
2. Select your API
3. Click the **Permissions** tab
4. Add permissions matching your scope names (e.g., `read:users`, `write:users`)
### Request Tokens with Scopes
```bash
curl -X POST https://your-tenant.auth0.com/oauth/token \
-H "Content-Type: application/json" \
-d '{
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"audience": "https://my-springboot-api",
"grant_type": "client_credentials",
"scope": "read:users write:users"
}'
```
---
## DPoP Authentication
[DPoP](https://www.rfc-editor.org/rfc/rfc9449.html) (Demonstrating Proof of Possession) binds tokens to a specific client key pair, preventing token theft.
### Configuration Modes
#### ALLOWED Mode (Default)
Accepts both Bearer and DPoP tokens:
```yaml
auth0:
domain: "your-tenant.auth0.com"
audience: "https://my-springboot-api"
dpop-mode: ALLOWED
```
#### `REQUIRED` Mode
Only accepts DPoP tokens — rejects standard Bearer:
```yaml
auth0:
domain: "your-tenant.auth0.com"
audience: "https://my-springboot-api"
dpop-mode: REQUIRED
```
#### DISABLED Mode
Standard JWT Bearer only — rejects DPoP tokens:
```yaml
auth0:
domain: "your-tenant.auth0.com"
audience: "https://my-springboot-api"
dpop-mode: DISABLED
```
### Fine-Tuning DPoP Time Validation (Optional)
The defaults work for most use cases. Only adjust these if you need to handle clock skew or network delays:
```yaml
auth0:
domain: "your-tenant.auth0.com"
audience: "https://my-springboot-api"
dpop-mode: ALLOWED
dpop-iat-offset-seconds: 300 # Optional: max age of DPoP proof (default: 300)
dpop-iat-leeway-seconds: 30 # Optional: additional time leeway (default: 30)
```
### How DPoP Works in Controllers
DPoP validation is handled by the `Auth0AuthenticationFilter` before the request reaches your controller. Your controller code is the same regardless of whether the client used Bearer or DPoP:
```java
@GetMapping("/sensitive")
public ResponseEntity<Map<String, Object>> sensitiveEndpoint(Authentication authentication) {
// Works the same for both Bearer and DPoP tokens
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();
}
```
### DPoP WWW-Authenticate Headers
The library automatically generates RFC-compliant `WWW-Authenticate` headers on failures:
```http
# 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"
```
### Enable DPoP on Auth0 API
1. Go to Auth0 Dashboard → Applications → APIs
2. Select your API
3. Enable DPoP binding requirement
---
## Accessing User Claims
### From Controller Parameter
```java
@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();
}
```
### Common JWT Claims
| Claim | Description |
|-------|-------------|
| `sub` | User ID (subject) |
| `scope` | Space-separated list of granted scopes |
| `aud` | Audience (your API identifier) |
| `iss` | Issuer (your Auth0 tenant URL) |
| `exp` | Expiration timestamp |
| `iat` | Issued-at timestamp |
Custom claims added via Auth0 Actions use namespaced keys, e.g., `https://example.com/role`.
---
## Error Handling
### BaseAuthException Hierarchy
The library uses `BaseAuthException` subclasses for different error conditions:
| Exception | HTTP Status | Cause |
|-----------|-------------|-------|
| `MissingAuthorizationException` | 400 | No or multiple `Authorization` headers |
| `VerifyAccessTokenException` | 401 | JWT validation failed (expired, bad signature, wrong audience) |
| `InvalidAuthSchemeException` | 400 | Wrong auth scheme for configured DPoP mode |
| `InvalidDpopProofException` | 400 | DPoP proof validation failed |
| `InsufficientScopeException` | 403 | Valid token but missing required scope |
The `Auth0AuthenticationFilter` handles all exceptions automatically, setting the appropriate HTTP status and `WWW-Authenticate` header. No custom exception handling is needed in controllers for auth errors.
### Custom Error Responses
For non-auth errors in your controllers, use standard Spring patterns:
```java
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleError(Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", e.getMessage()));
}
```
### Standard Error Responses
| Status | Cause | Fix |
|--------|-------|-----|
| 401 | Missing or invalid token | Include valid `Authorization: Bearer <token>` header |
| 401 | Expired token | Request a fresh access token |
| 401 | Wrong audience | Token's `aud` claim must match your API Identifier |
| 403 | Insufficient scope | Token must include required scopes |
---
## Mixed Public and Protected Endpoints
```java
@RestController
@RequestMapping("/api")
public class MixedController {
// Public - no auth needed
@GetMapping("/public")
public ResponseEntity<Map<String, Object>> publicEndpoint() {
return ResponseEntity.ok(Map.of("message", "Public endpoint"));
}
// Protected - requires valid JWT
@GetMapping("/private")
public ResponseEntity<Map<String, Object>> privateEndpoint(Authentication authentication) {
return ResponseEntity.ok(Map.of(
"message", "Private endpoint",
"user", authentication.getName()
));
}
// Protected with scope
@GetMapping("/messages")
@PreAuthorize("hasAuthority('SCOPE_read:messages')")
public ResponseEntity<Map<String, Object>> messagesEndpoint() {
return ResponseEntity.ok(Map.of("messages", List.of("Hello", "World")));
}
}
```
---
## CORS Configuration
For APIs consumed by browser-based SPAs, configure CORS **before** the auth filter:
```java
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain apiSecurity(
HttpSecurity http,
Auth0AuthenticationFilter authFilter
) throws Exception {
return http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public").permitAll()
.anyRequest().authenticated())
.addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type", "DPoP"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
}
```
---
## Testing
### Integration Testing with MockMvc
```java
@SpringBootTest
@AutoConfigureMockMvc
class ApiControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void publicEndpoint_returns200() throws Exception {
mockMvc.perform(get("/api/public"))
.andExpect(status().isOk());
}
@Test
void protectedEndpoint_withoutToken_returns401() throws Exception {
mockMvc.perform(get("/api/protected"))
.andExpect(status().isUnauthorized());
}
}
```
### Testing with curl
```bash
# Get a test token
TOKEN=$(auth0 test token --audience https://my-springboot-api --json | jq -r '.access_token')
# Test protected endpoint
curl http://localhost:8080/api/protected \
-H "Authorization: Bearer $TOKEN"
```
---
## Security Considerations
- **Stateless sessions** — Always use `SessionCreationPolicy.STATELESS` for API endpoints
- **No client secret** — This library validates JWTs via JWKS; no client secret is stored or needed
- **CORS before auth** — Configure CORS middleware before the auth filter in the security chain
- **Use HTTPS in production** — Auth0 requires HTTPS for token issuance
- **Minimal scopes** — Only enforce scopes your API actually needs
- **DPoP for high-security** — Enable `dpop-mode: REQUIRED` for APIs handling sensitive data
---
---
# Auth0 Spring Boot API Setup Guide
Setup instructions for Spring Boot API applications using `auth0-springboot-api`.
---
## Auth0 Configuration
> **Agent instruction:**
>
> **Check if Auth0 domain and audience are already in the user's prompt first.**
> If the prompt contains Auth0 domain and audience, use them directly — skip to "Write Configuration" below. Do NOT call `AskUserQuestion` to re-confirm.
>
> **If Auth0 configuration is NOT provided**, use `AskUserQuestion` to ask:
> "How would you like to configure Auth0?"
> - Option A: "Automatic setup using Auth0 CLI (recommended)"
> - Option B: "Manual setup" — provide domain and audience manually
>
> **If Automatic Setup:**
>
> 1. **Pre-flight checks:**
> - Verify Auth0 CLI is installed: `auth0 --version`
> - Verify logged in: `auth0 tenants list --csv --no-input`
> - If any check fails, guide user to install/login, or fall back to manual setup
>
> 2. **Create the API resource using Auth0 CLI:**
> ```bash
> auth0 apis create --name "My Spring Boot API" --identifier https://my-springboot-api --json
> ```
> Then write the returned domain and audience to `application.yml`.
>
> **If Manual Setup:**
>
> Ask the user for:
> - Auth0 Domain (e.g., `your-tenant.auth0.com`)
> - API Audience / Identifier (e.g., `https://my-springboot-api`)
>
> Write the configuration file with provided values.
---
## Quick Setup (Automated)
Uses the Auth0 CLI to create an Auth0 API resource and configure your project.
### Step 1: Install Auth0 CLI and create API resource
```bash
# Install Auth0 CLI (macOS)
brew install auth0
# Login
auth0 login --no-input
# Create an Auth0 API resource
auth0 apis create \
--name "My Spring Boot API" \
--identifier https://my-springboot-api \
--json
```
Note the `identifier` value — this is your Audience.
### Step 2: Get your domain
```bash
auth0 tenants list
```
Your domain is shown in the output (e.g., `your-tenant.auth0.com`).
### Step 3: Write configuration
Add to `src/main/resources/application.yml`:
```yaml
auth0:
domain: "your-tenant.auth0.com"
audience: "https://my-springboot-api"
```
Or `src/main/resources/application.properties`:
```properties
auth0.domain=your-tenant.auth0.com
auth0.audience=https://my-springboot-api
```
---
## Manual Setup
### Install Dependency
**Gradle (build.gradle):**
```groovy
implementation 'com.auth0:auth0-springboot-api:1.0.0-beta.1'
```
**Maven (pom.xml):**
```xml
<dependency>
<groupId>com.auth0</groupId>
<artifactId>auth0-springboot-api</artifactId>
<version>1.0.0-beta.1</version>
</dependency>
```
### Create Auth0 API Resource
1. Go to Auth0 Dashboard → Applications → APIs
2. Click **Create API**
3. Set a **Name** and an **Identifier** (e.g., `https://my-springboot-api`)
4. Note the Identifier — this is your `audience`
### Configure application.yml
```yaml
auth0:
domain: "your-tenant.auth0.com"
audience: "https://my-springboot-api"
```
**Important:** Domain format is `your-tenant.auth0.com` — do NOT include `https://`.
### Get Auth0 Configuration
- **Domain:** Auth0 Dashboard → Settings → Domain (or `auth0 tenants list`)
- **Audience:** The identifier you set when creating the API resource
---
## Post-Setup Steps
1. **Verify audience matches** — The `auth0.audience` value must exactly match your API Identifier in Auth0 Dashboard
2. **Add SecurityConfig** — Create a `SecurityConfig.java` class with `Auth0AuthenticationFilter` added before `UsernamePasswordAuthenticationFilter`
3. **Build and test** — Run `./gradlew bootRun` (or `./mvnw spring-boot:run`) and test endpoints
---
## Environment-Specific Configuration
This library validates JWTs via JWKS (public key verification). **No client secret is needed.**
The `domain` and `audience` values are not secrets — they are public identifiers. However, they typically differ per environment:
### Development
Use `application.yml` or `application.properties` directly:
```yaml
auth0:
domain: "your-tenant.auth0.com"
audience: "https://my-springboot-api"
```
### Production
Use environment variables (override `application.yml`):
```bash
export AUTH0_DOMAIN=your-tenant.auth0.com
export AUTH0_AUDIENCE=https://my-springboot-api
```
Or use Spring profiles (`application-prod.yml`).
---
## Getting a Test Token
### Via Auth0 Dashboard
1. Go to Auth0 Dashboard → Applications → APIs
2. Select your API
3. Click the **Test** tab
4. Click **Copy Token** to get a test access token
### Via Auth0 CLI (Client Credentials)
```bash
auth0 test token \
--audience https://my-springboot-api
```
### Via curl (Client Credentials Flow)
```bash
curl -X POST https://your-tenant.auth0.com/oauth/token \
-H "Content-Type: application/json" \
-d '{
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"audience": "https://my-springboot-api",
"grant_type": "client_credentials"
}'
```
---
## Verification
1. Application starts without errors: `./gradlew bootRun`
2. Public endpoint accessible without token: `curl http://localhost:8080/api/public`
3. Protected endpoint returns 401 without token: `curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/protected`
4. Protected endpoint returns 200 with valid token
---
## Troubleshooting
**401 Unauthorized - "invalid_token":** Verify that the `auth0.audience` in config exactly matches your API Identifier in Auth0 Dashboard.
**401 Unauthorized - "invalid_issuer":** Ensure `auth0.domain` does not include `https://` — use `your-tenant.auth0.com` format only.
**No Auth0AuthenticationFilter bean found:** Ensure `auth0-springboot-api` dependency is on the classpath and both `auth0.domain` and `auth0.audience` are configured.
**Token expired:** Test tokens from the Dashboard are short-lived. Request a fresh token.
---
SHA-256: a19b0024831aadb612943d0f57f4c4c049dd3f916900d18e092cefaf4eb12578