How authentication actually resolves

Spring Security · lesson 28 of 31 · 6 min read

AuthenticationManager, ProviderManager and the delegation chain behind a login.

Open this lesson in the learning hub

Key points

  • A filter does not authenticate. It builds an unauthenticated Authentication token and hands it to the AuthenticationManager - the filter knows how to read a request, nothing more.
  • The usual manager is a ProviderManager, which holds a list of AuthenticationProviders and asks each whether it supports the token type, stopping at the first that can handle it.
  • That is the extension point. A custom provider - API keys, a legacy password store, a hardware token - plugs in without touching any filter.
  • A provider returns a fully authenticated token with authorities, or throws. Returning null means "not mine", and the manager moves on to the next provider.
  • The result is placed in the SecurityContextHolder, which is a ThreadLocal by default - so it does not follow work onto an @Async thread or an executor without explicit propagation.
  • Erase credentials after authenticating. ProviderManager does this by default, and turning it off leaves passwords sitting in the security context for the life of the request.

Example

/*
 * THE CHAIN, top to bottom:
 *
 *   UsernamePasswordAuthenticationFilter
 *     -> builds UsernamePasswordAuthenticationToken (unauthenticated)
 *     -> AuthenticationManager.authenticate(token)
 *          -> ProviderManager loops its providers
 *               -> DaoAuthenticationProvider.supports(token)?  yes
 *                    -> UserDetailsService.loadUserByUsername()
 *                    -> PasswordEncoder.matches()
 *                    -> returns an AUTHENTICATED token with authorities
 *     -> SecurityContextHolder.setContext(...)
 */

// A custom provider - no filter changes needed.
@Component
class ApiKeyAuthenticationProvider implements AuthenticationProvider {

    private final ApiKeyService keys;

    ApiKeyAuthenticationProvider(ApiKeyService keys) { this.keys = keys; }

    @Override
    public Authentication authenticate(Authentication authentication) {
        String presented = (String) authentication.getCredentials();

        // Constant-time compare inside: a length-sensitive equals leaks
        // the key one character at a time under timing analysis.
        ApiKey key = keys.findValid(presented)
                .orElseThrow(() -> new BadCredentialsException("Unknown API key"));

        return new ApiKeyAuthenticationToken(key.owner(), key.authorities());  // authenticated
    }

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

// The context does NOT follow work onto another thread.
@Bean
DelegatingSecurityContextAsyncTaskExecutor securityAwareExecutor(ThreadPoolTaskExecutor d) {
    // Without this, an @Async method sees an empty SecurityContext and every
    // authorization check inside it fails or silently sees an anonymous user.
    return new DelegatingSecurityContextAsyncTaskExecutor(d);
}

Filters read requests, providers authenticate - add a provider rather than a filter, and remember the context is a ThreadLocal.

This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Spring Security course, and every lesson in it is listed on the Spring Security contents page.