Home/Learn/Spring Boot/Custom Authentication

Custom Authentication

Intermediate
Security

UserDetailsService loads user data during authentication. Implement it to load users from your database, hash passwords with BCrypt, and wire everything into Spring Security's DaoAuthenticationProvider.

Overview

Spring Security's form-login and HTTP-basic authentication work through DaoAuthenticationProvider, which calls your UserDetailsService.loadUserByUsername() to retrieve user details, then compares the stored password hash with the submitted password using a PasswordEncoder. You implement UserDetailsService to connect Spring Security to your User entity. BCryptPasswordEncoder with a cost factor of 10-12 is the recommended password hashing algorithm.

UserDetails and UserDetailsService

UserDetails is an interface Spring Security uses to represent an authenticated user — it provides username, password hash, authorities, and account status flags. Implement it on your User entity or create a separate wrapper.

Java — UserDetails wrapper and UserDetailsService
// Option 1: Wrap your User entity in UserDetails
public class UserPrincipal implements UserDetails {
    private final User user;

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

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

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole().name()));
    }

    @Override public boolean isAccountNonExpired()  { return true; }
    @Override public boolean isAccountNonLocked()   { return user.isActive(); }
    @Override public boolean isCredentialsNonExpired() { return true; }
    @Override public boolean isEnabled()             { return user.isEmailVerified(); }

    public String getId() { return user.getId(); } // expose for JWT generation
}

// UserDetailsService — Spring calls this during authentication
@Service
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        return userRepository.findByEmail(email)
            .map(UserPrincipal::new)
            .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email));
    }
}

PasswordEncoder and AuthenticationProvider

BCrypt is the standard — it embeds the salt in the hash and is intentionally slow. Wire the UserDetailsService and PasswordEncoder into DaoAuthenticationProvider and register it with the AuthenticationManager.

Java — BCryptPasswordEncoder, DaoAuthenticationProvider, login
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomUserDetailsService userDetailsService;

    public SecurityConfig(CustomUserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12); // cost factor 12 — ~300ms per hash
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }

    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}

// Registration — always hash before saving
@Service
public class AuthService {
    private final PasswordEncoder passwordEncoder;
    private final UserRepository userRepository;

    public User register(RegisterRequest req) {
        String hash = passwordEncoder.encode(req.password()); // BCrypt hash
        User user = User.builder()
            .email(req.email())
            .passwordHash(hash)
            .role(Role.USER)
            .build();
        return userRepository.save(user);
    }

    // Login — return JWT after successful authentication
    public String login(LoginRequest req) {
        Authentication auth = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(req.email(), req.password())
        ); // throws BadCredentialsException if wrong password
        UserPrincipal principal = (UserPrincipal) auth.getPrincipal();
        return jwtService.generateToken(principal.getId());
    }
}

Key Points to Remember

  • 1UserDetailsService.loadUserByUsername() is called by Spring Security to load user details during authentication.
  • 2UserDetails provides password hash, authorities, and account status — Spring Security handles the comparison.
  • 3BCryptPasswordEncoder embeds the salt in the hash — never store raw or MD5/SHA-hashed passwords.
  • 4Cost factor 10-12 is recommended — it makes BCrypt slow enough to resist brute force.
  • 5Throw UsernameNotFoundException from loadUserByUsername — Spring converts it to BadCredentialsException.
  • 6Always use passwordEncoder.matches(raw, encoded) to check passwords — never compare hashes directly.

Interview Questions

Sign in to ask Aria
1

What is UserDetailsService and when is it called by Spring Security?

EasyTCS
2

Why do we use BCrypt for password hashing instead of SHA-256?

MediumAmazon
3

What does the cost factor in BCryptPasswordEncoder control?

MediumRazorpay
4

What exception should loadUserByUsername throw if the user is not found?

EasyWipro
5

How does Spring Security compare the submitted password with the stored hash?

MediumThoughtWorks

Ask Aria about Custom Authentication

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…