Skip to content

Latest commit

 

History

History
492 lines (375 loc) · 14.6 KB

File metadata and controls

492 lines (375 loc) · 14.6 KB

Auth0 Spring Boot API Examples

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:

  • ALLOWED mode: Accepts both Authorization: Bearer <token> and Authorization: DPoP <token> + DPoP: <proof>
  • REQUIRED mode: Rejects Bearer tokens — only DPoP tokens with a valid proof are accepted
  • DISABLED mode: Rejects DPoP tokens — only Bearer tokens 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:

  1. Multi-brand applications (B2C) where each brand uses a different custom domain but they all share the same API.
  2. A single API serves multiple frontend applications that use different custom domains.
  3. 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, not AUTH0_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"