Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 50

Database Authentication with Custom UserDetailsService

Recommended resources

Hello! Welcome back to our module on "Spring Security & REST API Design."

In our last lesson, we demystified Spring Security's architecture, exploring how the FilterChainProxy intercepts requests and how the SecurityContextHolder tracks the authenticated user. We saw that authentication is handled by components like the AuthenticationManager working within this filter chain.

Today, we will connect that architecture to a real-world data source. Your learning outcome is to implement authentication against a database by creating a custom UserDetailsService. This is a fundamental skill for any backend developer and a common topic in senior-level Java interviews. We'll replace Spring's default in-memory user with a system that can look up users from your application's database.

1. The Authentication Puzzle: Connecting to Your Data

In the previous lesson's architecture, we saw that the AuthenticationManager delegates to an AuthenticationProvider. For username/password authentication, this is typically the DaoAuthenticationProvider. The "DAO" (Data Access Object) part is the key: this provider needs to access your user data.

However, DaoAuthenticationProvider has no idea if your users are in a MySQL database, a PostgreSQL database, or even a simple file. To bridge this gap, Spring Security defines two crucial interfaces:

  • UserDetailsService: A service interface with a single method, loadUserByUsername(String username). Your job is to implement this method to fetch user data from your specific storage. It acts as a DAO for Spring Security.
  • UserDetails: An interface that represents the user's core security information. Your UserDetailsService must return an object that implements this interface. It serves as an adapter between your application's user model (e.g., a JPA entity) and the data Spring Security requires, such as the username, the hashed password, and a list of authorities (roles/permissions).

The overall flow looks like this:

Spring Security Authentication Flow with Custom UserDetailsService
This flowchart shows how an authentication request flows through the system. The `AuthenticationManager` uses an `AuthenticationProvider` (like `DaoAuthenticationProvider`), which in turn calls your custom `UserDetailsService` to fetch user details. The provider then uses a `PasswordEncoder` to validate the password before returning a complete `Authentication` principal.

2. The Core Components in Detail

To fully grasp the concept before we dive into code, let's get a clear definition of the components involved.

Spring Security: Authentication and Authorization In-Depth

This article provides an excellent, concise explanation of the scenario where you have access to user passwords (e.g., in a database) and need to integrate with Spring Security.

Please read the sections titled '1. UserDetailsService: Having access to the user’s password', 'PasswordEncoders', and the summary 'Summary: Having access to the user’s password'. Focus on understanding the distinct roles of UserDetailsService and PasswordEncoder and why both are necessary.

The key takeaways from this reading are:

  1. If you manage your own users, you must provide a UserDetailsService bean.
  2. This service's job is to load user data by username only. Spring Security handles the password comparison.
  3. You must also provide a PasswordEncoder bean so Spring Security knows which hashing algorithm to use. Storing plain-text passwords is a critical security vulnerability.

3. Step-by-Step Implementation

Now, let's build a complete database authentication solution. We'll follow a common and clean approach using the Decorator pattern, where we create a dedicated UserDetails implementation that wraps our JPA User entity.

This video provides a practical, step-by-step guide that we will follow. I'll break down the key segments and add context.

[Spring Boot Security] #19 Database Authentication - Implement User Details Service

This video by Dan Geabunea demonstrates how to implement a custom UserDetailsService from scratch. We will use it to guide our implementation.

Watch the video from the beginning to 12:40. It covers the entire process of creating the UserDetails and UserDetailsService implementations. I'll highlight the most important steps below.

Step 3.1: Create the UserDetails Adapter (The Decorator)

(Video segment: 01:54 - 09:12)

Instead of making your JPA @Entity class implement UserDetails directly, it's often cleaner to create a separate class. This separates your domain model from security-specific concerns.

  1. Create a UserPrincipal class that implements UserDetails.
  2. Add a private final User field (your JPA entity) to this class and initialize it in the constructor. This is the "decorating" part.
  3. Implement the UserDetails methods (getUsername, getPassword, etc.) by delegating the calls to the wrapped User entity (e.g., return this.user.getPassword();).
  4. Implement getAuthorities(). This is crucial. You need to convert the roles and/or permissions from your User entity into a Collection<GrantedAuthority>. A common way is to create SimpleGrantedAuthority objects. Remember the convention: roles are typically prefixed with ROLE_ (e.g., ROLE_ADMIN). The video demonstrates combining two separate lists (roles and permissions) into one list of authorities.
// Example UserPrincipal
public class UserPrincipal implements UserDetails {

    private final User user; // Your JPA User entity

    public UserPrincipal(User user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // Example: converting a Set<Role> from your User entity
        return user.getRoles().stream()
                   .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
                   .collect(Collectors.toList());
    }

    @Override
    public String getPassword() {
        return user.getPassword(); // The hashed password from the DB
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    // For simplicity, other methods can return true
    @Override
    public boolean isAccountNonExpired() { return true; }
    // ... etc.
}

Step 3.2: Implement the UserDetailsService

(Video segment: 09:12 - 12:40)

This is the service that Spring Security will call.

  1. Create a @Service class (e.g., CustomUserDetailsService) that implements UserDetailsService.
  2. Inject your UserRepository (e.g., JpaRepository<User, Long>).
  3. Implement the loadUserByUsername(String username) method:
    • Use the injected repository to find the user in the database (e.g., userRepository.findByUsername(username)).
    • If the user is not found, you must throw a UsernameNotFoundException. Returning null violates the interface contract and can cause unexpected errors.
    • If the user is found, wrap the User entity in your UserPrincipal adapter and return it: return new UserPrincipal(user);.
Test your understanding!

An interviewer asks: "In your UserDetailsService implementation, what should happen if a user with the given username doesn't exist in the database?"

Show answer

Your implementation should throw a UsernameNotFoundException. This is a specific AuthenticationException subclass that Spring Security is designed to handle. It signals a failed authentication attempt due to an invalid username. Returning null or a generic exception is incorrect and breaks the UserDetailsService contract.

Step 3.3: Configure the PasswordEncoder and AuthenticationProvider

Now we need to tell Spring Security to use our new components. The modern, component-based approach in Spring Security 6+ makes this very clean.

Implementing Database Authentication and Authorization ...

This article from GeeksForGeeks shows how to configure the security components using the latest Spring Security 6 style, which avoids the deprecated WebSecurityConfigurerAdapter.

Review the code in the section titled 'Final and Important Step: Configure Spring Security'. Focus on how the AuthenticationProvider bean is created and configured with the UserDetailsService and PasswordEncoder.

In your @Configuration class for security, you'll define two essential beans:

  1. PasswordEncoder Bean: This tells Spring Security how to hash and verify passwords. BCryptPasswordEncoder is the strong, modern standard.

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
  2. AuthenticationProvider Bean: This bean explicitly wires together your custom UserDetailsService and the PasswordEncoder.

    @Bean
    public AuthenticationProvider authenticationProvider(UserService userService) { // Assuming your service is named UserService
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userService); // Here's the connection!
        provider.setPasswordEncoder(passwordEncoder()); // And here!
        return provider;
    }
    

By defining these beans, Spring Boot's auto-configuration will pick them up and use them in the authentication process. You have now successfully plugged your database into the security architecture.


Conclusion

In this lesson, you've bridged the gap between Spring Security's abstract architecture and a concrete database implementation. This is a critical pattern for building secure, real-world applications and a must-know for technical interviews.

Key Takeaways:

  • UserDetailsService is the contract you implement to tell Spring Security how to load a user from your data store.
  • UserDetails is the contract that adapts your application's user model into a format Spring Security can understand.
  • The Decorator Pattern (using a wrapper class like UserPrincipal) is a clean way to implement UserDetails without cluttering your domain entity.
  • You must always provide a PasswordEncoder bean (like BCryptPasswordEncoder) to securely handle passwords.
  • These components are wired together in your security configuration by defining an AuthenticationProvider bean that uses your custom UserDetailsService and PasswordEncoder.

Preview of the Next Lesson:
Our current setup relies on form-login and sessions, which is great for traditional web applications. However, modern REST APIs, especially in a microservices context, often need to be stateless. In our next lesson, we'll address this by learning how to explain the structure of a JSON Web Token (JWT) and its role in stateless authentication. We'll prepare to replace session-based security with token-based security.