Skip to content

How to Integrate Auth0 with Spring Security in Spring Boot: Complete Guide with JWT Examples

When I first tried integrating Auth0 with Spring Security in a Spring Boot application, I was confused about the right approach. Should I use Auth0’s Java SDK? Implement OAuth2 login? Or configure it as a resource server? After extensive research and implementation, I found that configuring Spring Boot as an OAuth2 Resource Server is the cleanest and most secure approach for modern JWT-based authentication.

In this guide, I’ll show you how to properly integrate Auth0 with Spring Security using JWT tokens, implement user synchronization, handle custom authorities, and avoid common pitfalls that can compromise your application’s security.

Understanding the Architecture

Before diving into code, let me clarify the architecture. In this setup:

  1. Auth0 acts as the Authorization Server (issuing JWT tokens)
  2. Spring Boot acts as an OAuth2 Resource Server (validating tokens)
  3. Your frontend (React, Angular, mobile app) handles authentication with Auth0 and sends JWT tokens to your API
┌─────────────┐ 1. Login ┌─────────────┐
│ │ ───────────────────> │ │
│ Frontend │ │ Auth0 │
│ (React/Ang)│ <─────────────────── │ (AuthZ │
│ │ 2. JWT Token │ Server) │
└─────────────┘ └─────────────┘
│ 3. API Request + JWT
┌─────────────────────────────────────┐
│ │
│ Spring Boot Resource Server │
│ ┌───────────────────────────────┐ │
│ │ Spring Security Filter │ │
│ │ Chain (JWT Validation) │ │
│ └───────────────────────────────┘ │
│ ┌───────────────────────────────┐ │
│ │ User Synchronization │ │
│ │ Service │ │
│ └───────────────────────────────┘ │
│ ┌───────────────────────────────┐ │
│ │ Business Logic │ │
│ └───────────────────────────────┘ │
│ │
└─────────────────────────────────────┘
┌─────────────┐
│ Database │
│ (Users) │
└─────────────┘

This architecture keeps your backend stateless while still maintaining a local user database for application-specific data and roles.

Step 1: Add Maven Dependencies

First, I added the necessary Spring Security dependencies. The key dependency is spring-boot-starter-oauth2-resource-server, which provides JWT validation support.

pom.xml
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security OAuth2 Resource Server -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<!-- Spring Data JPA for user synchronization -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Database driver (PostgreSQL in this example) -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>

I used spring-boot-starter-oauth2-resource-server instead of spring-boot-starter-security because I wanted explicit control over which security features are included. This starter includes Spring Security’s JWT validation capabilities without unnecessary OAuth2 login components.

Step 2: Configure Auth0 Settings

Next, I configured Auth0 settings in application.yml. I needed to specify the JWT issuer URI and the audience (API identifier) that Auth0 uses to validate tokens.

src/main/resources/application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
# Auth0 domain with /.well-known/jwks.json suffix
issuer-uri: https://your-tenant.auth0.com/
# Your Auth0 API Identifier (audience)
audiences:
- https://your-api.example.com
# JWKS endpoint for token validation
jwk-set-uri: https://your-tenant.auth0.com/.well-known/jwks.json
datasource:
url: jdbc:postgresql://localhost:5432/yourdb
username: yourdbuser
password: yourdbpassword
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
# Custom application properties
auth0:
domain: your-tenant.auth0.com
api-identifier: https://your-api.example.com

The critical configuration here is the issuer-uri. Spring Security uses this to:

  1. Fetch the JWKS (JSON Web Key Set) from Auth0’s /.well-known/jwks.json endpoint
  2. Validate that JWT tokens were issued by your Auth0 tenant
  3. Automatically rotate signing keys when Auth0 rotates them

I also configured custom audiences to ensure that only tokens intended for my API are accepted. This prevents tokens issued for other APIs from being used against your application.

Step 3: Implement Security Configuration

The security configuration is where I defined how JWT tokens are validated and converted to Spring Security authentication objects. I created a SecurityConfig class that configures the security filter chain.

src/main/java/com/example/config/SecurityConfig.java
package com.example.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Value("${auth0.audience}")
private String audience;
@Value("${spring.security.oauth2.resourceserver.jwt.issuer-uri}")
private String issuer;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// Disable CSRF for stateless JWT authentication
.csrf(csrf -> csrf.disable())
// Configure CORS
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// Stateless session (no server-side sessions)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// Configure endpoint authorization
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
// Configure OAuth2 Resource Server with JWT
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
);
return http.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter =
new JwtGrantedAuthoritiesConverter();
// Extract authorities from custom "permissions" claim
grantedAuthoritiesConverter.setAuthoritiesClaimName("permissions");
grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");
JwtAuthenticationConverter jwtAuthenticationConverter =
new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(
grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://localhost:3000"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

Key configuration decisions I made:

  1. Stateless Session: JWT-based authentication should be stateless. The server doesn’t store session data, making it scalable.
  2. Custom Authorities: I configured JwtAuthenticationConverter to extract authorities from Auth0’s permissions claim instead of the default scope claim.
  3. CORS Configuration: Needed for frontend applications running on different origins.
  4. Method Security: @EnableMethodSecurity allows using @PreAuthorize annotations on methods.

Step 4: Create User Entity and Repository

Even though JWT contains user information, I still needed a local user database for application-specific data and roles. I created a User entity that syncs with Auth0 users.

src/main/java/com/example/entity/User.java
package com.example.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String auth0Id; // Auth0 user ID (sub claim)
@Column(nullable = false)
private String email;
private String name;
private String picture;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "role")
private Set<String> roles = new HashSet<>();
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Constructors
public User() {}
public User(String auth0Id, String email) {
this.auth0Id = auth0Id;
this.email = email;
}
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getAuth0Id() { return auth0Id; }
public void setAuth0Id(String auth0Id) { this.auth0Id = auth0Id; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getPicture() { return picture; }
public void setPicture(String picture) { this.picture = picture; }
public Set<String> getRoles() { return roles; }
public void setRoles(Set<String> roles) { this.roles = roles; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
}

I stored the auth0Id (the JWT’s sub claim) as a unique identifier to link Auth0 users with local database records. This allows me to:

  1. Sync user data on first login
  2. Store application-specific roles and permissions
  3. Maintain relationships with other entities

Step 5: Implement User Synchronization Service

The user synchronization service automatically creates or updates local user records when a JWT token is validated. This is crucial for maintaining a local user database.

src/main/java/com/example/service/UserSynchronizationService.java
package com.example.service;
import com.example.entity.User;
import com.example.repository.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class UserSynchronizationService {
private static final Logger logger = LoggerFactory.getLogger(UserSynchronizationService.class);
private final UserRepository userRepository;
public UserSynchronizationService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional
public User syncUserFromJwt(Jwt jwt) {
String auth0Id = jwt.getSubject(); // 'sub' claim
String email = jwt.getClaimAsString("email");
String name = jwt.getClaimAsString("name");
String picture = jwt.getClaimAsString("picture");
// Extract permissions/roles from JWT
Set<String> permissions = jwt.getClaimAsStringList("permissions")
.stream()
.collect(Collectors.toSet());
return userRepository.findByAuth0Id(auth0Id)
.map(existingUser -> {
// Update existing user
existingUser.setEmail(email);
existingUser.setName(name);
existingUser.setPicture(picture);
existingUser.setRoles(permissions);
logger.debug("Updated user: {}", existingUser.getEmail());
return userRepository.save(existingUser);
})
.orElseGet(() -> {
// Create new user
User newUser = new User(auth0Id, email);
newUser.setName(name);
newUser.setPicture(picture);
newUser.setRoles(permissions);
logger.info("Created new user: {}", newUser.getEmail());
return userRepository.save(newUser);
});
}
}

This service syncs user data every time a JWT is validated. I chose to update the user on each request because:

  1. Auth0 profile data (name, picture) can change
  2. Permissions might be updated in Auth0
  3. Keeping local data in sync ensures consistency

If performance becomes an issue, I can cache the user data or sync only periodically.

Step 6: Create Custom UserPrincipal

I created a custom UserPrincipal that combines JWT claims with database user data. This gives me the best of both worlds: JWT validation plus application-specific roles.

src/main/java/com/example/security/CustomUserPrincipal.java
package com.example.security;
import com.example.entity.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import java.util.Collection;
import java.util.stream.Collectors;
public class CustomUserPrincipal {
private final Jwt jwt;
private final User user;
public CustomUserPrincipal(Jwt jwt, User user) {
this.jwt = jwt;
this.user = user;
}
public String getAuth0Id() {
return jwt.getSubject();
}
public String getEmail() {
return jwt.getClaimAsString("email");
}
public String getName() {
return jwt.getClaimAsString("name");
}
public String getPicture() {
return jwt.getClaimAsString("picture");
}
public User getDatabaseUser() {
return user;
}
public Jwt getJwt() {
return jwt;
}
public Collection<? extends GrantedAuthority> getAuthorities() {
// Combine JWT permissions with database roles
// Prioritize database roles for application-specific permissions
return user.getRoles()
.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toSet());
}
public boolean hasRole(String role) {
return user.getRoles().contains(role);
}
}

Step 7: Implement Custom UserDetailsService

I implemented a custom UserDetailsService that wraps the JWT authentication and syncs user data. This integrates user synchronization with Spring Security’s authentication flow.

src/main/java/com/example/service/CustomUserDetailsService.java
package com.example.service;
import com.example.entity.User;
import com.example.security.CustomUserPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Service;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserSynchronizationService userSynchronizationService;
public CustomUserDetailsService(UserSynchronizationService userSynchronizationService) {
this.userSynchronizationService = userSynchronizationService;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// This method is not used in JWT-based authentication
// But we implement it for UserDetailsService interface
throw new UnsupportedOperationException("Use loadUserByJwt instead");
}
public CustomUserPrincipal loadUserByJwt(Jwt jwt) {
User user = userSynchronizationService.syncUserFromJwt(jwt);
return new CustomUserPrincipal(jwt, user);
}
}

Actually, with JWT authentication, Spring Security doesn’t call UserDetailsService directly. Instead, I needed to integrate the user synchronization into the JWT authentication converter. Let me show you the updated approach.

Step 8: Create Custom JWT Converter with User Sync

I created a custom JWT converter that syncs the user before returning the authentication object. This ensures user data is always in sync.

src/main/java/com/example/security/CustomJwtAuthenticationConverter.java
package com.example.security;
import com.example.entity.User;
import com.example.service.UserSynchronizationService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.stream.Collectors;
@Component
public class CustomJwtAuthenticationConverter
implements Converter<Jwt, AbstractAuthenticationToken> {
private final UserSynchronizationService userSynchronizationService;
public CustomJwtAuthenticationConverter(
UserSynchronizationService userSynchronizationService) {
this.userSynchronizationService = userSynchronizationService;
}
@Override
public AbstractAuthenticationToken convert(Jwt jwt) {
// Sync user from JWT
User user = userSynchronizationService.syncUserFromJwt(jwt);
// Extract authorities
Collection<GrantedAuthority> authorities = user.getRoles()
.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toSet());
return new JwtAuthenticationToken(jwt, authorities);
}
}

Then I updated the SecurityConfig to use this custom converter:

src/main/java/com/example/config/SecurityConfig.java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ... other configuration ...
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(customJwtAuthenticationConverter)
)
);
return http.build();
}

Step 9: Create REST Controller

Now I created a controller that uses the authenticated user information. I used @AuthenticationPrincipal to access the JWT and user data.

src/main/java/com/example/controller/UserController.java
package com.example.controller;
import com.example.entity.User;
import com.example.repository.UserRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserRepository userRepository;
public UserController(UserRepository userRepository) {
this.userRepository = userRepository;
}
@GetMapping("/me")
public ResponseEntity<Map<String, Object>> getCurrentUser(
@AuthenticationPrincipal Jwt jwt) {
String auth0Id = jwt.getSubject();
User user = userRepository.findByAuth0Id(auth0Id)
.orElseThrow(() -> new RuntimeException("User not found"));
return ResponseEntity.ok(Map.of(
"id", user.getId(),
"auth0Id", user.getAuth0Id(),
"email", user.getEmail(),
"name", user.getName(),
"picture", user.getPicture(),
"roles", user.getRoles(),
"createdAt", user.getCreatedAt()
));
}
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<List<User>> getAllUsers() {
List<User> users = userRepository.findAll();
return ResponseEntity.ok(users);
}
@PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<User> updateUser(
@PathVariable Long id,
@RequestBody UserUpdateRequest request) {
User user = userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("User not found"));
if (request.getName() != null) {
user.setName(request.getName());
}
if (request.getPicture() != null) {
user.setPicture(request.getPicture());
}
return ResponseEntity.ok(userRepository.save(user));
}
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userRepository.deleteById(id);
return ResponseEntity.noContent().build();
}
}
class UserUpdateRequest {
private String name;
private String picture;
// Getters and setters
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getPicture() { return picture; }
public void setPicture(String picture) { this.picture = picture; }
}

The @AuthenticationPrincipal Jwt jwt parameter gives me access to the JWT token, and I can then use it to fetch the database user. I also used @PreAuthorize annotations for method-level security.

Step 10: Implement Exception Handling

JWT authentication errors need proper handling to return meaningful error messages. I created an exception handler for JWT-related errors.

src/main/java/com/example/exception/JwtExceptionHandler.java
package com.example.exception;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtExceptionHandler
implements AuthenticationEntryPoint, AccessDeniedHandler {
private static final Logger logger = LoggerFactory.getLogger(JwtExceptionHandler.class);
private final BearerTokenAuthenticationEntryPoint delegate =
new BearerTokenAuthenticationEntryPoint();
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
logger.error("Authentication error: {}", authException.getMessage());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
Map<String, Object> body = new HashMap<>();
body.put("status", HttpServletResponse.SC_UNAUTHORIZED);
body.put("error", "Unauthorized");
body.put("message", "Invalid or expired JWT token");
body.put("path", request.getServletPath());
new ObjectMapper().writeValue(response.getOutputStream(), body);
}
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
logger.error("Access denied: {}", accessDeniedException.getMessage());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
Map<String, Object> body = new HashMap<>();
body.put("status", HttpServletResponse.SC_FORBIDDEN);
body.put("error", "Forbidden");
body.put("message", "You don't have permission to access this resource");
body.put("path", request.getServletPath());
new ObjectMapper().writeValue(response.getOutputStream(), body);
}
}

Then I registered the exception handler in SecurityConfig:

src/main/java/com/example/config/SecurityConfig.java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
JwtExceptionHandler jwtExceptionHandler) throws Exception {
http
// ... other configuration ...
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(jwtExceptionHandler)
.accessDeniedHandler(jwtExceptionHandler)
);
return http.build();
}

Step 11: Write Tests

Testing JWT-secured endpoints requires mocking JWT tokens. Spring Security provides testing utilities that make this straightforward.

src/test/java/com/example/controller/UserControllerTest.java
package com.example.controller;
import com.example.entity.User;
import com.example.repository.UserRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
import org.springframework.test.web.servlet.MockMvc;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserRepository userRepository;
private User testUser;
@BeforeEach
void setUp() {
Set<String> roles = new HashSet<>();
roles.add("USER");
testUser = new User("auth0|123456", "[email protected]");
testUser.setId(1L);
testUser.setName("Test User");
testUser.setRoles(roles);
when(userRepository.findByAuth0Id(anyString()))
.thenReturn(Optional.of(testUser));
}
@Test
void testGetCurrentUser_Unauthorized() throws Exception {
mockMvc.perform(get("/api/users/me"))
.andExpect(status().isUnauthorized());
}
@Test
void testGetCurrentUser_WithValidJwt() throws Exception {
mockMvc.perform(get("/api/users/me")
.with(SecurityMockMvcRequestPostProcessors.jwt()
.jwt(jwt -> jwt
.subject("auth0|123456")
.claim("email", "[email protected]")
.claim("name", "Test User")
)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.email").value("[email protected]"))
.andExpect(jsonPath("$.name").value("Test User"));
}
@Test
void testGetAllUsers_AsAdmin() throws Exception {
Set<String> roles = new HashSet<>();
roles.add("ADMIN");
mockMvc.perform(get("/api/users")
.with(SecurityMockMvcRequestPostProcessors.jwt()
.jwt(jwt -> jwt.subject("auth0|admin"))
.authorities(() -> "ROLE_ADMIN")))
.andExpect(status().isOk());
}
@Test
void testGetAllUsers_AsUser_Forbidden() throws Exception {
mockMvc.perform(get("/api/users")
.with(SecurityMockMvcRequestPostProcessors.jwt()
.jwt(jwt -> jwt.subject("auth0|user"))
.authorities(() -> "ROLE_USER")))
.andExpect(status().isForbidden());
}
}

I used SecurityMockMvcRequestPostProcessors.jwt() to create mock JWT tokens for testing. This allows me to test different scenarios without needing a real Auth0 server.

Step 12: Configure Auth0 Application

Finally, I needed to configure the Auth0 application and API. Here’s what I did in the Auth0 Dashboard:

Create API in Auth0

  1. Navigate to APIs > Create API
  2. Set Name: “My Spring Boot API”
  3. Set Identifier: “https://your-api.example.com” (this becomes the audience)
  4. Set Signing Algorithm: RS256

Configure Permissions

In the API settings, I added permissions:

  • read:users - Read user information
  • write:users - Modify user information
  • admin - Administrative access

Create Application

  1. Navigate to Applications > Create Application
  2. Choose Single Page Application for React/Angular frontend
  3. Configure Allowed Callback URLs: http://localhost:3000/callback
  4. Configure Allowed Logout URLs: http://localhost:3000
  5. Configure Allowed Web Origins: http://localhost:3000

Add Custom Claims (Optional)

To include custom claims in the JWT, I created a Rule in Auth0:

Auth0 Rule - Add Custom Claims
function (user, context, callback) {
const namespace = 'https://your-api.example.com/';
context.idToken[namespace + 'roles'] = context.authorization.roles;
context.accessToken[namespace + 'roles'] = context.authorization.roles;
callback(null, user, context);
}

This adds a roles claim to the JWT, which I can then use in my application.

Common Mistakes to Avoid

During my implementation, I encountered several pitfalls. Here are the most common mistakes and how to avoid them:

1. Not Validating the Audience

The biggest security mistake is not validating the aud (audience) claim. Without audience validation, tokens issued for other APIs can be used against your application.

Incorrect - No Audience Validation
// DON'T DO THIS!
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
Correct - With Audience Validation
// DO THIS!
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
);
return http.build();
}
// Also configure in application.yml
// spring.security.oauth2.resourceserver.jwt.audiences: https://your-api.example.com

2. Storing Sensitive Data in JWT

JWT tokens are base64-encoded, not encrypted. Anyone can decode and read the contents. Never store:

  • Passwords
  • API keys
  • Credit card numbers
  • Personal identifiable information (PII) beyond what’s necessary

3. Hardcoding Auth0 Domain

Never hardcode the Auth0 domain in your code. Use environment variables or configuration files:

Correct - Use Configuration
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: ${AUTH0_ISSUER:https://your-tenant.auth0.com/}

4. Not Handling Token Expiration

JWT tokens have expiration times. Your application should handle expired tokens gracefully:

Token Expiration Handler
@Component
public class JwtExceptionHandler implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
if (authException instanceof JwtException) {
// Token expired or invalid
response.sendError(HttpServletResponse.SC_UNAUTHORIZED,
"{\"error\": \"Token expired\", \"code\": \"TOKEN_EXPIRED\"}");
}
}
}

5. Synchronous User Synchronization on Every Request

While I showed user sync on every request in the example, for high-traffic applications, this can become a bottleneck. Consider:

  1. Caching user data with Redis
  2. Syncing only on first login and periodically thereafter
  3. Using a separate background job for user sync

Verification and Testing

To verify the integration works, I tested with curl:

Test JWT Authentication
# Get a token from Auth0 (using your frontend application or Auth0 CLI)
export TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6..."
# Test the /api/users/me endpoint
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:8080/api/users/me
# Expected response:
# {
# "id": 1,
# "auth0Id": "auth0|123456",
# "email": "[email protected]",
# "name": "Test User",
# "roles": ["USER"]
# }

I also used the Auth0 CLI to get test tokens:

Get Test Token from Auth0 CLI
# Install Auth0 CLI
brew install auth0-cli
# Login
auth0 login
# Get test token
auth0 test token --audience https://your-api.example.com

Summary

Integrating Auth0 with Spring Security in Spring Boot involves:

  1. Adding spring-boot-starter-oauth2-resource-server dependency
  2. Configuring JWT issuer URI and audience in application.yml
  3. Creating a SecurityFilterChain bean with JWT authentication
  4. Implementing user synchronization to sync JWT claims to local database
  5. Creating custom authentication converters for custom authorities
  6. Handling JWT exceptions gracefully
  7. Writing comprehensive tests with mock JWT tokens

The key insight is that Spring Boot acts as a resource server, not an OAuth2 client. Auth0 handles user authentication, and your Spring Boot application validates JWT tokens and extracts user information.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments