AuthenticationManager, AuthenticationProvider, and UserDetailsService

The Authentication Delegation Chain

Spring Security separates requesting authentication from performing authentication. A filter receives a username and password — but it does not check them itself. It passes the request to AuthenticationManager, which delegates to one or more AuthenticationProvider instances:

flowchart LR
    F[Authentication Filter\nForm Login / Basic / JWT] -->|UsernamePasswordToken| AM[AuthenticationManager\nProviderManager]
    AM -->|try each provider| AP1[DaoAuthenticationProvider\nfor DB users]
    AM -->|try each provider| AP2[LdapAuthenticationProvider\nfor LDAP users]
    AM -->|try each provider| AP3[Custom Provider\nfor API key]
    AP1 -->|if supported| UDS[UserDetailsService\nloadUserByUsername]
    UDS --> DB[(Database)]
    AP1 -->|verify password| PE[PasswordEncoder]
    AP1 -->|success| Token[Authenticated Token]
    Token --> F

This chain is the core of Spring Security’s extensibility — you can plug in any number of authentication mechanisms without touching the filter or the rest of the framework.


AuthenticationManager

AuthenticationManager has a single method:

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

Input: an unauthenticated Authentication (e.g., UsernamePasswordAuthenticationToken with username and plaintext password).

Output: an authenticated Authentication (same type, but with isAuthenticated() = true and populated authorities), or an AuthenticationException if authentication fails.

The standard implementation is ProviderManager.


ProviderManager: The Standard AuthenticationManager

ProviderManager holds a list of AuthenticationProvider instances. When authenticate() is called, it iterates through the list and tries each provider:

flowchart TD
    PM[ProviderManager] -->|for each provider| Check{supports this\nAuthentication type?}
    Check -->|no| Next[try next provider]
    Check -->|yes| Try[provider.authenticate]
    Try -->|success| Return[return authenticated token]
    Try -->|AuthenticationException| Next
    Next --> Check
    Next -->|no more providers| Parent{parent\nProviderManager?}
    Parent -->|yes| PM2[parent.authenticate]
    Parent -->|no| Throw[throw ProviderNotFoundException]
// ProviderManager selects providers by type
public boolean supports(Class<?> authentication) {
    return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}

Parent AuthenticationManager

ProviderManager can have a parent. If none of its own providers can handle the token, it delegates to the parent. This supports hierarchical authentication — e.g., per-HttpSecurity managers that fall back to a global one.


AuthenticationProvider

AuthenticationProvider does the actual credential verification:

public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
    boolean supports(Class<?> authentication); // which token types can this handle?
}

DaoAuthenticationProvider

The most common provider — handles UsernamePasswordAuthenticationToken by loading the user from a UserDetailsService and verifying the password with a PasswordEncoder:

sequenceDiagram
    participant PM as ProviderManager
    participant DAP as DaoAuthenticationProvider
    participant UDS as UserDetailsService
    participant PE as PasswordEncoder

    PM->>DAP: authenticate(token{username="alice", password="secret"})
    DAP->>UDS: loadUserByUsername("alice")
    UDS-->>DAP: UserDetails{username="alice", password="$2a$...", roles=[ROLE_USER]}
    DAP->>PE: matches("secret", "$2a$...")
    PE-->>DAP: true
    DAP->>DAP: check account non-expired, non-locked, credentials valid
    DAP-->>PM: UsernamePasswordAuthToken{principal=UserDetails, authorities=[ROLE_USER]}

The password is matched against the stored (hashed) password. If they match and the account is in good standing, an authenticated token is returned with the user’s authorities.


UserDetailsService

UserDetailsService is the bridge between Spring Security and your database:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

It has one job: given a username, return a UserDetails object (or throw UsernameNotFoundException). Spring Security handles everything else — password checking, account status validation, authority population.

Database-Backed Implementation

@Service
@RequiredArgsConstructor
public class AppUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsernameIgnoreCase(username)
            .orElseThrow(() -> new UsernameNotFoundException(
                "User not found: " + username
            ));

        return org.springframework.security.core.userdetails.User.builder()
            .username(user.getUsername())
            .password(user.getPasswordHash())
            .roles(user.getRoles().stream()
                .map(Role::getName)
                .toArray(String[]::new))
            .accountExpired(!user.isActive())
            .accountLocked(user.isLocked())
            .credentialsExpired(user.isPasswordExpired())
            .disabled(!user.isEnabled())
            .build();
    }
}

Using a Custom UserDetails

For richer user data (like the user ID needed for authorization), use a custom UserDetails:

@Service
@RequiredArgsConstructor
public class AppUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    @Transactional(readOnly = true)
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByEmailIgnoreCase(username)  // email as username
            .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
        return new AppUserDetails(user); // custom UserDetails with getId(), getEmail(), etc.
    }
}

UserDetailsPasswordService

An optional extension for upgrading stored password hashes without requiring users to re-authenticate:

public interface UserDetailsPasswordService {
    UserDetails updatePassword(UserDetails user, String newPassword);
}

When DaoAuthenticationProvider detects that the stored password is encoded with an outdated algorithm (via PasswordEncoder.upgradeEncoding()), it calls updatePassword() with the re-encoded hash. Implement this on your UserDetailsService to transparently migrate password hashes.


Wiring It Together: The Security Configuration

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final AppUserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

    // Expose AuthenticationManager as a bean (needed for manual authenticate() calls)
    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

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

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authenticationProvider(authenticationProvider())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated()
            )
            .csrf(csrf -> csrf.disable())
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
        return http.build();
    }
}

Why Expose AuthenticationManager as a Bean?

Spring Boot auto-configures the AuthenticationManager internally. But if you need to call authenticationManager.authenticate(token) manually — for example, in a login endpoint that generates a JWT — you need it as an injectable bean:

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final JwtService jwtService;

    @PostMapping("/login")
    public JwtResponse login(@RequestBody @Valid LoginRequest request) {
        // Manually trigger authentication
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                request.getEmail(),
                request.getPassword()
            )
        );

        // Authentication succeeded — generate JWT
        AppUserDetails user = (AppUserDetails) authentication.getPrincipal();
        String token = jwtService.generateToken(user);
        return new JwtResponse(token);
    }
}

Custom AuthenticationProvider

Build a custom provider for non-standard authentication — API keys, hardware tokens, custom protocols:

@Component
public class ApiKeyAuthenticationProvider implements AuthenticationProvider {

    private final ApiKeyRepository apiKeyRepository;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        ApiKeyAuthenticationToken token = (ApiKeyAuthenticationToken) authentication;
        String apiKey = (String) token.getCredentials();

        ApiKeyEntity entity = apiKeyRepository.findByKeyHash(hashKey(apiKey))
            .orElseThrow(() -> new BadCredentialsException("Invalid API key"));

        if (!entity.isActive()) {
            throw new DisabledException("API key is disabled");
        }

        return new ApiKeyAuthenticationToken(
            entity,                    // principal
            apiKey,                    // credentials
            entity.getAuthorities()    // authorities from the API key's associated roles
        );
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return ApiKeyAuthenticationToken.class.isAssignableFrom(authentication);
    }

    private String hashKey(String key) {
        // SHA-256 hash of the key — never store raw API keys
        return DigestUtils.sha256Hex(key);
    }
}
// Custom token type
public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken {

    private final Object principal;
    private final String credentials;

    // Pre-authentication (unauthenticated)
    public ApiKeyAuthenticationToken(String apiKey) {
        super(null);
        this.principal = null;
        this.credentials = apiKey;
        setAuthenticated(false);
    }

    // Post-authentication (authenticated)
    public ApiKeyAuthenticationToken(Object principal, String credentials,
                                     Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(true);
    }

    @Override public Object getPrincipal() { return principal; }
    @Override public Object getCredentials() { return credentials; }
}

Multiple AuthenticationProviders

Register multiple providers to support multiple authentication mechanisms in one chain:

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authenticationProvider(daoAuthenticationProvider())    // username/password
        .authenticationProvider(apiKeyAuthenticationProvider()) // API keys
        .authenticationProvider(ldapAuthenticationProvider())   // LDAP
        ...
    return http.build();
}

ProviderManager tries each provider in order until one succeeds or all fail.


AuthenticationException Types

Different failures throw different exceptions — all extend AuthenticationException:

classDiagram
    AuthenticationException <|-- BadCredentialsException
    AuthenticationException <|-- UsernameNotFoundException
    AuthenticationException <|-- DisabledException
    AuthenticationException <|-- LockedException
    AuthenticationException <|-- AccountExpiredException
    AuthenticationException <|-- CredentialsExpiredException
    AuthenticationException <|-- ProviderNotFoundException

    class BadCredentialsException {
        Wrong password
    }
    class UsernameNotFoundException {
        User not found
    }
    class DisabledException {
        Account disabled
    }
    class LockedException {
        Account locked
    }

Security note: UsernameNotFoundException is internally converted to BadCredentialsException by DaoAuthenticationProvider by default (controlled by hideUserNotFoundExceptions). This prevents username enumeration — an attacker can’t tell whether a username doesn’t exist or the password is wrong.

// Keep username enumeration protection (default = true, recommended)
provider.setHideUserNotFoundExceptions(true);

UserDetails Account Status Checks

DaoAuthenticationProvider checks all four boolean flags on UserDetails before authentication succeeds:

public interface UserDetails {
    boolean isAccountNonExpired();    // false → AccountExpiredException
    boolean isAccountNonLocked();     // false → LockedException
    boolean isCredentialsNonExpired(); // false → CredentialsExpiredException
    boolean isEnabled();              // false → DisabledException
}

Use these for: account suspension (disabled), security lockout after failed attempts (locked), forced password rotation (credentialsExpired), account expiry for trial users (accountExpired).


Summary

flowchart TD
    Filter[Authentication Filter] -->|unauthenticated token| PM[ProviderManager\nAuthenticationManager]
    PM -->|iterate providers| P1[DaoAuthenticationProvider]
    PM -->|iterate providers| P2[Custom Provider]
    P1 -->|loadUserByUsername| UDS[UserDetailsService]
    UDS --> DB[(Database)]
    P1 -->|verify password| PE[PasswordEncoder]
    P1 -->|check account flags| Flags[isEnabled, isLocked...]
    P1 -->|success| Auth[Authenticated Token\nwith UserDetails + Authorities]
    Auth --> Filter
    Filter -->|setAuthentication| SCH[SecurityContextHolder]
  • AuthenticationManager (implemented by ProviderManager) coordinates — it doesn’t authenticate itself.
  • AuthenticationProvider does the actual verification — supports() determines which token types it handles.
  • UserDetailsService.loadUserByUsername() is the bridge to your database.
  • DaoAuthenticationProvider chains UserDetailsService + PasswordEncoder + account status checks.
  • Expose AuthenticationManager as a bean when you need to call authenticate() manually (e.g., JWT login endpoint).
  • hideUserNotFoundExceptions = true (default) prevents username enumeration attacks.

Next: Article 5 covers the modern SecurityFilterChain configuration API — every HttpSecurity option explained with real examples.