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:
- Auth0 acts as the Authorization Server (issuing JWT tokens)
- Spring Boot acts as an OAuth2 Resource Server (validating tokens)
- 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.
<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.
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 propertiesauth0: domain: your-tenant.auth0.com api-identifier: https://your-api.example.comThe critical configuration here is the issuer-uri. Spring Security uses this to:
- Fetch the JWKS (JSON Web Key Set) from Auth0’s
/.well-known/jwks.jsonendpoint - Validate that JWT tokens were issued by your Auth0 tenant
- 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.
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@EnableMethodSecuritypublic 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:
- Stateless Session: JWT-based authentication should be stateless. The server doesn’t store session data, making it scalable.
- Custom Authorities: I configured
JwtAuthenticationConverterto extract authorities from Auth0’spermissionsclaim instead of the defaultscopeclaim. - CORS Configuration: Needed for frontend applications running on different origins.
- Method Security:
@EnableMethodSecurityallows using@PreAuthorizeannotations 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.
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:
- Sync user data on first login
- Store application-specific roles and permissions
- 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.
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;
@Servicepublic 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:
- Auth0 profile data (name, picture) can change
- Permissions might be updated in Auth0
- 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.
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.
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;
@Servicepublic 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.
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;
@Componentpublic 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:
@Beanpublic 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.
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.
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;
@Componentpublic 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:
@Beanpublic 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.
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@AutoConfigureMockMvcclass UserControllerTest {
@Autowired private MockMvc mockMvc;
@MockBean private UserRepository userRepository;
private User testUser;
@BeforeEach void setUp() { Set<String> roles = new HashSet<>(); roles.add("USER");
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("name", "Test User") ))) .andExpect(status().isOk()) .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
- Navigate to APIs > Create API
- Set Name: “My Spring Boot API”
- Set Identifier: “https://your-api.example.com” (this becomes the
audience) - Set Signing Algorithm: RS256
Configure Permissions
In the API settings, I added permissions:
read:users- Read user informationwrite:users- Modify user informationadmin- Administrative access
Create Application
- Navigate to Applications > Create Application
- Choose Single Page Application for React/Angular frontend
- Configure Allowed Callback URLs:
http://localhost:3000/callback - Configure Allowed Logout URLs:
http://localhost:3000 - Configure Allowed Web Origins:
http://localhost:3000
Add Custom Claims (Optional)
To include custom claims in the JWT, I created a Rule in Auth0:
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.
// DON'T DO THIS!@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())); return http.build();}// DO THIS!@Beanpublic 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.com2. 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:
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:
@Componentpublic 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:
- Caching user data with Redis
- Syncing only on first login and periodically thereafter
- Using a separate background job for user sync
Verification and Testing
To verify the integration works, I tested with curl:
# Get a token from Auth0 (using your frontend application or Auth0 CLI)export TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6..."
# Test the /api/users/me endpointcurl -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:
# Install Auth0 CLIbrew install auth0-cli
# Loginauth0 login
# Get test tokenauth0 test token --audience https://your-api.example.comSummary
Integrating Auth0 with Spring Security in Spring Boot involves:
- Adding
spring-boot-starter-oauth2-resource-serverdependency - Configuring JWT issuer URI and audience in
application.yml - Creating a
SecurityFilterChainbean with JWT authentication - Implementing user synchronization to sync JWT claims to local database
- Creating custom authentication converters for custom authorities
- Handling JWT exceptions gracefully
- 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