There is a rite of passage every Spring Boot developer knows too well. You need to protect an API endpoint. "How hard can it be?" you think. Three hours later, you are deep in a StackOverflow thread about JSON Web Key Set (JWKS) rotation, wondering why your custom BearerTokenResolver is silently swallowing errors.
JWT validation should not be a side quest.
We are releasing auth0-springboot-api, a library that gives you production-grade Auth0 security in two lines of config. It handles JWT validation, JWKS caching, scope mapping, error responses, and, here is the interesting part, built-in Demonstration of Proof-of-Possession (DPoP) support support to stop stolen-token replay attacks.
Let's see what "minimal configuration" for securing a Spring Boot API with JWTs actually looks like with Auth0.
What You Are Not Writing Anymore
Before we look at the solution, let's be honest about what securing a Spring Boot API with JWTs actually involves. Even with Spring Security's resource server support, you are still on the hook for:
- Fetching and caching JWKS public keys from your Auth0 domain
- Validating JWT signatures (RSA256)
- Verifying
issandaudclaims against your tenant - Mapping token scopes to Spring Security authorities
- Returning standards-compliant
WWW-Authenticateheaders on failure
That is five concerns before you write a single line of business logic. And if you need DPoP (RFC 9449)? Add DPoP proof parsing, ES256 signature verification, JWK thumbprint binding, and time-based nonce validation to the list.
All of this is now handled for you. Here is how.
Four Steps to a Secured Spring Boot API
Let's walk through the four steps to secure your Spring Boot API with Auth0.
1. Add the dependency
Add the library to your project using Maven or Gradle. This single dependency brings in everything you need: JWT validation, JWKS key management, scope mapping, and DPoP support.
<!-- Maven --> <dependency> <groupId>com.auth0</groupId> <artifactId>auth0-springboot-api</artifactId> <version>1.0.0-beta.0</version> </dependency>
// Gradle implementation 'com.auth0:auth0-springboot-api:1.0.0-beta.0'
2. Point it at your Auth0 tenant
Two properties. That is all the library needs, it auto-discovers JWKS endpoints, issuer URLs, and signing algorithms from your domain:
auth0: domain: "your-tenant.auth0.com" audience: "https://your-api-identifier"
3. Wire up the security filter
The library auto-configures an Auth0AuthenticationFilter bean. You just drop it into your filter chain:
@Configuration public class SecurityConfig { @Bean SecurityFilterChain apiSecurity(HttpSecurity http, Auth0AuthenticationFilter authFilter) throws Exception { return http .csrf(csrf -> csrf.disable()) .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/protected").authenticated() .anyRequest().permitAll()) .addFilterBefore(authFilter, UsernamePasswordAuthenticationFilter.class) .build(); } }
No decoder beans, converter classes, or JWKS configuration. The filter is auto-configured and injected by Spring Boot.
4. Write your endpoints
With the security configuration in place, let's define a sample public and a protected endpoint to see it all in action.
@RestController public class ApiController { @GetMapping("/api/public") public String publicEndpoint() { return "Anyone can see this."; } @GetMapping("/api/protected") public String protectedEndpoint(Authentication authentication) { return "Hello, " + authentication.getName() + "!"; } }
That is a complete, secured API. The library validates signatures, checks claims, maps scopes, and returns proper error responses, all behind that one filter.
Now the Interesting Part: DPoP
Here is a question worth pausing on: What happens if someone intercepts one of your Bearer tokens?
The answer is uncomfortable. They can replay it. From any device, any network, any location. A Bearer token is a key whoever holds it walks in.
DPoP changes the equation. It cryptographically binds the token to the client that requested it. Think of the difference this way:
- Bearer token = a house key. Whoever has it can walk in.
- DPoP token = a house key + fingerprint scanner. Having the key alone is not enough.
Enabling it takes one line:
auth0: domain: "your-tenant.auth0.com" audience: "https://your-api-identifier" dpopMode: ALLOWED # ← that is it
Your API now accepts both Bearer and DPoP-bound tokens. No changes to controllers, no new annotations. The library validates DPoP proofs at the filter level before requests ever reach your code.
Three enforcement levels
The library supports three DPoP modes, so you can start simple and tighten security as your needs evolve
| Mode | Behavior | When to use |
|---|---|---|
| DISABLED | Bearer tokens only | Standard APIs without DPoP requirements |
| ALLOWED | Accepts both Bearer and DPoP | Rolling out DPoP gradually: existing clients keep working |
| REQUIRED | DPoP only, rejects Bearer | High-security APIs: financial services, healthcare, compliance-driven |
The recommended path: start with ALLOWED, migrate clients, then flip to REQUIRED. Zero downtime, zero controller changes.
For details on generating DPoP proofs on the client side, see the Auth0 DPoP documentation.
Working with Claims and Scopes
Token validation tells you who is calling. Scopes tell you what they are allowed to do.
Reading JWT claims
Cast to Auth0AuthenticationToken and pull any claim directly, no manual decoding required:
@GetMapping("/api/profile") public Map<String, Object> profile(Authentication authentication) { Auth0AuthenticationToken token = (Auth0AuthenticationToken) authentication; return Map.of( "subject", token.getClaim("sub"), "scopes", token.getScopes() ); }
Enforcing scopes
The library maps JWT scopes to Spring Security authorities with a SCOPE_ prefix. A token with "read:users write:users" produces SCOPE_read:users and SCOPE_write:users.
You can enforce scopes at the route level in your security filter chain:
.authorizeHttpRequests(auth -> auth .requestMatchers("/api/admin/**") .hasAuthority("SCOPE_admin") .requestMatchers("/api/users/**") .hasAuthority("SCOPE_read:users") .anyRequest().authenticated())
Or you can also enforce scopes at the method level using @PreAuthorize:
@GetMapping("/api/users") @PreAuthorize("hasAuthority('SCOPE_read:users')") public List<User> getUsers() { return userService.getAllUsers(); }
Method-level security requires @EnableMethodSecurity on a configuration class. See the Spring Security docs.
What Happens Under the Hood
You do not need to know this to use the library. But if you are the kind of developer who opens the hood before driving, here is the request lifecycle in five steps:
- Extract: The filter pulls the
Authorizationheader (andDPoPheader, if present). - Select strategy: Based on
dpopMode, it picks Bearer-only, DPoP-only, or auto-detect. - Validate JWT: Fetches JWKS keys, verifies RSA256 signature, checks
issandaud. - Validate DPoP proof: (if applicable) Verifies ES256 signature, checks HTTP method/URL binding, confirms JWK thumbprint match.
- Populate SecurityContext: Creates an
Auth0AuthenticationTokenwith the sub claim as principal and scopes asSCOPE_*authorities.
If anything fails, you get a 401 or 403 with a standards-compliant WWW-Authenticate header. Your controllers never see invalid tokens.
Try It in 60 Seconds
You can try it yourself using the auth0-auth-java repo, which includes a ready-to-run playground app. If you already have an Auth0 tenant, you can be testing authenticated requests in under a minute.
# Clone and configure git clone https://github.com/auth0/auth0-auth-java.git cd auth0-auth-java # Edit auth0-springboot-api-playground/src/main/resources/application.yml # with your Auth0 domain and audience # Run it ./gradlew :auth0-springboot-api-playground:bootRun
Then test:
# Public endpoint — no token needed curl http://localhost:8080/api/public # Protected endpoint — get a token first curl -X POST https://YOUR_DOMAIN/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" }' # Use the token curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ http://localhost:8080/api/protected
Curious what is inside the token? Paste the access_token value into jwt.io to see the decoded claims.
The entire playground is three files: a Spring Boot entry point, a 12-line security config, and a controller with two endpoints. No JWT decoder beans. No JWKS configuration classes.
New to Auth0? The Spring Boot API Quickstart walks you through creating an API in the Auth0 Dashboard, configuring a machine-to-machine app, and testing your endpoints step by step.
What Is Next
This is a beta release, and we shipped it early intentionally. We want it shaped by real-world usage, not just internal testing.
If you hit a rough edge, have a feature idea, or just want to tell us what worked. We are listening:
Security should not be the hardest part of your Spring Boot app. Go build something or sign up and get started.
Resources
- GitHub Repository
- Spring Boot API Quickstart
- Examples and Patterns
- Auth0 DPoP Documentation
- RFC 9449 — DPoP Specification
- Sign up for a free Auth0 account
Quick Reference
| Task | How |
|---|---|
| Add the dependency | com.auth0:auth0-springboot-api:1.0.0-beta.0 |
| Configure Auth0 | domain + audience in application.yml |
| Enable DPoP | Add dpopMode: ALLOWED |
| Get user identity | authentication.getName() |
| Read any claim | auth0Token.getClaim("email") |
| Enforce scopes | .hasAuthority("SCOPE_read:users") or @PreAuthorize | |
| Run the playground | ./gradlew :auth0-springboot-api-playground:bootRun | |
About the author

Tanya Sinha
Senior Software Engineer
