announcements

Secure Your Spring Boot API with Auth0 in Minutes

Secure your Spring Boot API in minutes using the new Auth0 Spring Boot starter. Includes built-in DPoP support to stop stolen-token replay attacks.

May 6, 20268 min read

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:

  1. Fetching and caching JWKS public keys from your Auth0 domain
  2. Validating JWT signatures (RSA256)
  3. Verifying iss and aud claims against your tenant
  4. Mapping token scopes to Spring Security authorities
  5. Returning standards-compliant WWW-Authenticate headers 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:

  1. Extract: The filter pulls the Authorization header (and DPoP header, if present).
  2. Select strategy: Based on dpopMode, it picks Bearer-only, DPoP-only, or auto-detect.
  3. Validate JWT: Fetches JWKS keys, verifies RSA256 signature, checks iss and aud.
  4. Validate DPoP proof: (if applicable) Verifies ES256 signature, checks HTTP method/URL binding, confirms JWK thumbprint match.
  5. Populate SecurityContext: Creates an Auth0AuthenticationToken with the sub claim as principal and scopes as SCOPE_* 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

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

Tanya Sinha

Senior Software Engineer

Tanya is a Senior Backend Engineer passionate about building scalable, secure, and high-performance backend systems. She works extensively on distributed services, authentication platforms, and API security. She enjoys turning complex backend challenges into simple, reliable solutions for end developers. Outside of engineering, she play guitar and loves trekking to recharge.View profile