Skip to content

Should You Maintain a Local User Table When Using Auth0?

Should You Maintain a Local User Table When Using Auth0?

When I first integrated Auth0 into my Spring Boot application, I asked myself: “Do I really need a local user table if Auth0 handles all the authentication?” This is a common question that trips up many developers new to OAuth2 and identity providers.

The short answer: Yes, you should maintain a local user table when using Auth0.

But the real question isn’t if you need one—it’s why you need one and how to design it correctly.

The Problem: Auth0 Alone Isn’t Enough

Auth0 is fantastic at what it does: authentication. It handles:

  • User credentials (email, password)
  • Password hashing and security
  • Social logins (Google, GitHub, Facebook)
  • Multi-factor authentication (MFA)
  • Session management
  • JWT token generation

However, Auth0 is not designed to manage your application’s specific data. It doesn’t know about:

  • User roles in your system (admin, moderator, regular user)
  • User preferences (theme, notifications)
  • Relationships (which users belong to which teams)
  • Business logic (subscription tier, feature flags)
  • Application-specific metadata

When you try to shoehorn all this into Auth0’s user metadata, you’re fighting the tool’s design.

Separation of Concerns

Here’s how I think about the division of responsibilities:

Separation of concerns diagram
┌─────────────────────────────────────┐ ┌─────────────────────────────────────┐
│ Auth0 (AuthN) │ │ Local DB (Application) │
├─────────────────────────────────────┤ ├─────────────────────────────────────┤
│ ✓ Email & password │ │ ✓ Application roles │
│ ✓ Social logins │ │ ✓ User preferences │
│ ✓ MFA setup │ │ ✓ Business relationships │
│ ✓ Session tokens │ │ ✓ Feature flags │
│ ✓ User metadata (basic) │ │ ✓ Audit logs │
│ │ │ ✓ Application state │
│ source of truth: identity │ │ source of truth: business data │
└─────────────────────────────────────┘ └─────────────────────────────────────┘
│ │
│ │
└─────────────┬─────────────────────────────┘
linked by: auth0_id (sub claim)

The key insight: Auth0 owns identity, your database owns application context.

The Anti-Patterns to Avoid

Before I show you the right way, let me highlight three mistakes I’ve seen:

1. Duplicating Passwords in Local DB

Never store passwords locally when using Auth0. Auth0 handles credential management—you should never see or store user passwords. This defeats the purpose of using an identity provider.

2. Using Email as Primary Key

Emails can change. Users update their email addresses. Auth0’s sub (subject) claim is guaranteed to be unique and immutable. Use it.

3. Calling Auth0 Management API on Every Request

I’ve seen developers query Auth0’s Management API to get user details on every request. This adds latency, consumes API rate limits, and makes your application dependent on Auth0’s availability. Cache what you need locally.

Implementation: Spring Boot + Auth0

Let me show you how I implement this pattern in Spring Boot.

SQL Schema for Local Users

First, create a local users table that references Auth0:

schema.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
auth0_id VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) NOT NULL,
name VARCHAR(255),
role VARCHAR(50) NOT NULL DEFAULT 'USER',
preferences JSONB DEFAULT '{}',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_auth0_id ON users(auth0_id);
CREATE INDEX idx_users_email ON users(email);

Notice:

  • auth0_id stores the sub claim from JWT (unique identifier)
  • email is stored for convenience but not used as primary key
  • role is application-specific (Auth0 doesn’t know about this)
  • preferences uses JSONB for flexibility (PostgreSQL feature)

JPA Entity

Map this to a JPA entity:

User.java
package com.example.app.entity;
import jakarta.persistence.*;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "auth0_id", unique = true, nullable = false)
private String auth0Id;
@Column(nullable = false)
private String email;
private String name;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private Role role = Role.USER;
@Column(columnDefinition = "jsonb")
@ElementCollection
private Map<String, Object> preferences = new HashMap<>();
@Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt;
@Column(name = "updated_at", nullable = false)
private Instant updatedAt;
@PrePersist
protected void onCreate() {
createdAt = Instant.now();
updatedAt = Instant.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = Instant.now();
}
// Getters and setters...
public Long getId() { return 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 Role getRole() { return role; }
public void setRole(Role role) { this.role = role; }
public Map<String, Object> getPreferences() { return preferences; }
public Instant getCreatedAt() { return createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
}
Role.java
package com.example.app.entity;
public enum Role {
USER,
MODERATOR,
ADMIN
}

Repository with Custom Queries

Create a repository with a method to find by Auth0 ID:

UserRepository.java
package com.example.app.repository;
import com.example.app.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByAuth0Id(String auth0Id);
Optional<User> findByEmail(String email);
boolean existsByAuth0Id(String auth0Id);
}

The Key Pattern: Just-in-Time User Creation

Here’s the critical piece—the “get or create” pattern that creates local users on first authentication:

UserService.java
package com.example.app.service;
import com.example.app.entity.User;
import com.example.app.repository.UserRepository;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Transactional
public User getOrCreateUser(Jwt jwt) {
String auth0Id = jwt.getSubject(); // The 'sub' claim
return userRepository.findByAuth0Id(auth0Id)
.orElseGet(() -> createNewUser(jwt));
}
private User createNewUser(Jwt jwt) {
User user = new User();
user.setAuth0Id(jwt.getSubject());
user.setEmail(jwt.getClaim("email"));
user.setName(jwt.getClaim("name"));
// Set default preferences
user.getPreferences().put("theme", "light");
user.getPreferences().put("notifications", true);
return userRepository.save(user);
}
public Optional<User> findByAuth0Id(String auth0Id) {
return userRepository.findByAuth0Id(auth0Id);
}
public User save(User user) {
return userRepository.save(user);
}
}

This pattern is beautiful because:

  1. No separate registration flow needed
  2. Users are created automatically on first login
  3. Subsequent logins fetch the existing user
  4. All application-specific data lives in your database

Security Configuration

Configure Spring Security to validate JWTs from Auth0:

SecurityConfig.java
package com.example.app.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt());
return http.build();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}

Configure your application.yml:

application.yml
spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://your-tenant.auth0.com/
audiences: your-api-audience

Controller: Accessing the Current User

Now create controllers that extract the user from the JWT:

UserController.java
package com.example.app.controller;
import com.example.app.entity.User;
import com.example.app.service.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/me")
public ResponseEntity<UserProfile> getCurrentUser(@AuthenticationPrincipal Jwt jwt) {
User user = userService.getOrCreateUser(jwt);
UserProfile profile = new UserProfile(
user.getId(),
user.getEmail(),
user.getName(),
user.getRole().name(),
user.getPreferences()
);
return ResponseEntity.ok(profile);
}
@PutMapping("/me/preferences")
public ResponseEntity<User> updatePreferences(
@AuthenticationPrincipal Jwt jwt,
@RequestBody Map<String, Object> preferences
) {
User user = userService.getOrCreateUser(jwt);
user.getPreferences().putAll(preferences);
User updated = userService.save(user);
return ResponseEntity.ok(updated);
}
public record UserProfile(
Long id,
String email,
String name,
String role,
Map<String, Object> preferences
) {}
}

Authorization: Using Roles

With the local user table, you can implement application-level authorization. Here’s an aspect that checks roles:

UserSecurity.java
package com.example.app.security;
import com.example.app.entity.User;
import com.example.app.service.UserService;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;
@Component("userSecurity")
public class UserSecurity {
private final UserService userService;
public UserSecurity(UserService userService) {
this.userService = userService;
}
public boolean hasRole(Authentication authentication, String role) {
if (authentication.getPrincipal() instanceof Jwt jwt) {
User user = userService.getOrCreateUser(jwt);
return user.getRole().name().equals(role);
}
return false;
}
public boolean isOwner(Authentication authentication, Long userId) {
if (authentication.getPrincipal() instanceof Jwt jwt) {
User user = userService.getOrCreateUser(jwt);
return user.getId().equals(userId);
}
return false;
}
}

Use it in your controllers:

DocumentController.java
package com.example.app.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/documents")
public class DocumentController {
@GetMapping("/{id}")
@PreAuthorize("@userSecurity.isOwner(authentication, #id) or @userSecurity.hasRole(authentication, 'ADMIN')")
public Document getDocument(@PathVariable Long id) {
// ...
}
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public void deleteDocument(@PathVariable Long id) {
// ...
}
}

When to Sync with Auth0?

Sometimes you need data from Auth0. Here’s when I sync:

  1. On first login (create local user with JWT claims)
  2. When user updates profile (sync back to Auth0 via Management API, optional)
  3. Never on every request (this would be wasteful)

If you need to sync user data back to Auth0, use the Management API:

Auth0ManagementService.java
package com.example.app.service;
import com.auth0.client.mgmt.ManagementAPI;
import com.auth0.client.mgmt.filter.UserFilter;
import com.auth0.json.mgmt.users.User;
import org.springframework.stereotype.Service;
@Service
public class Auth0ManagementService {
private final ManagementAPI managementAPI;
public Auth0ManagementService(ManagementAPI managementAPI) {
this.managementAPI = managementAPI;
}
public void updateUserInAuth0(String auth0Id, String name, String email) {
try {
User userUpdate = new User();
userUpdate.setName(name);
userUpdate.setEmail(email);
managementAPI.users().update(auth0Id, userUpdate, new UserFilter()).execute();
} catch (Exception e) {
// Log and handle error
throw new RuntimeException("Failed to update user in Auth0", e);
}
}
}

Best Practices Summary

Here’s what I’ve learned:

  1. Use sub claim as unique identifier - Never use email; it can change
  2. Just-in-time user creation - Create local users on first authentication
  3. Keep application data local - Roles, preferences, relationships stay in your DB
  4. Minimize Management API calls - Only sync when necessary
  5. Cache user data locally - Don’t call Auth0 on every request
  6. Handle edge cases - What if Auth0 user is deleted? Add cleanup jobs.

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