Spring Security: the Authentication and AuthenticationProvider Contracts
Objective
Understand the two contracts that sit underneath every authentication attempt in
Spring Security — Authentication, which represents a request in progress (or a
completed, successful login) and always answers "who is this and are they
authenticated?", and AuthenticationProvider, which owns the actual authentication
logic behind an AuthenticationManager — and the two-method design
(authenticate()/supports()) that lets several providers coexist and be tried
in turn without any of them needing to know about the others.
Use Cases
- Supporting more than one credential shape in the same application (username/password
and an SMS code, say) by registering one
AuthenticationProviderper shape and letting theAuthenticationManagerpick the one that fits. - Writing a custom
AuthenticationProviderthat still delegates toUserDetailsServiceandPasswordEncoderinternally — custom orchestration, standard building blocks — instead of reinventing user lookup and password checking from scratch. - Diagnosing an authentication request that silently fails or a
ProviderNotFoundException: understanding exactly when a provider should returnnullversus throw, and how that interacts with the other registered providers.
Deep Dive
The Authentication interface: representing a request, in progress or done
Authentication extends the JDK's own Principal, and is the object that flows
through the whole authentication process — before it succeeds, it holds the raw
credentials being checked; after it succeeds, it holds the authenticated identity
and its authorities:
javapublic interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated)
throws IllegalArgumentException;
}The three methods worth knowing first:
isAuthenticated()—falsewhile the request is still being validated,trueonce anAuthenticationProviderhas confirmed the credentials.getCredentials()— the secret being checked (a password, most commonly).getAuthorities()— the permissions granted to the request, populated once authentication succeeds.
Extending Principal (rather than inventing a parallel concept) is a deliberate
compatibility choice: any code written against the plain Java Security API's
Principal already understands half of what an Authentication object offers,
which eases migrating existing authentication code onto Spring Security.
The AuthenticationProvider contract: authenticate() and supports()
javapublic interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}authenticate() is where the actual logic lives, and it follows three rules:
- Throw
AuthenticationException(or a subclass, likeBadCredentialsException) if authentication fails outright. - Return
nullif theAuthenticationobject passed in isn't one this provider knows how to handle — this is what lets several providers, each built for a different credential type, sit side by side. - Return a fully authenticated
Authenticationinstance (isAuthenticated()true) on success — and, as good practice, one with the password/credential stripped out, since it's no longer needed and keeping it around is a needless exposure.
supports(Class<?> authentication) is a coarser, cheaper first filter: it answers
"do I handle this type of Authentication object at all?" before authenticate()
is even called. The two checks are deliberately separate — a provider can say yes
to supports() (the object is the right type) and still return null from
authenticate() (that specific instance isn't one it can validate), the same way a
lock built for cards might recognize "this is a card" but still reject a card from
a different building.
One manager, several providers
An AuthenticationManager doesn't authenticate anything itself — ProviderManager,
its default implementation, holds a list of AuthenticationProviders and delegates
to whichever one claims the request, trying each in turn (the book's own analogy:
a lock that accepts either a keycard or a physical key delegates to whichever
provider understands the object it was handed, and shrugs at neither). A provider
that doesn't recognize the type says so via supports() and is skipped; one that
recognizes the type but rejects the specific object returns null from
authenticate(), and the manager moves on to the next provider. If none of them
succeeds, authentication fails with ProviderNotFoundException.
Writing a custom provider that still uses UserDetailsService/PasswordEncoder
Implementing AuthenticationProvider from scratch doesn't mean abandoning the
existing building blocks — the book's own example wires the standard
UserDetailsService and PasswordEncoder into custom logic, rather than
replacing them:
java@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
UserDetails u = userDetailsService.loadUserByUsername(username);
if (passwordEncoder.matches(password, u.getPassword())) {
return new UsernamePasswordAuthenticationToken(
username, password, u.getAuthorities());
} else {
throw new BadCredentialsException("Something went wrong!");
}
}
@Override
public boolean supports(Class<?> authenticationType) {
return authenticationType
.equals(UsernamePasswordAuthenticationToken.class);
}
}supports() narrows this provider to standard username/password requests
(UsernamePasswordAuthenticationToken, the type produced when nothing custom is
configured at the HTTP-filter level). authenticate() loads the user, checks the
password, and either returns a fully authenticated token — authorities included,
ready for the SecurityContext — or throws. Marking the class @Component is
enough for Spring to find it; how it then gets wired into the provider chain is
covered next.
Trade-offs
nullvs. throw is a real design decision, not an implementation detail. Returningnullfromauthenticate()politely defers to the next provider; throwing ends the whole authentication attempt immediately. Makingsupports()too broad (claiming a type this provider can't really validate) forces it into awkwardnullreturns instead of a clean "not mine" viasupports().- Delegating to
UserDetailsService/PasswordEncoderfrom inside a custom provider is usually the better middle ground. It's tempting to reinvent user lookup and password checking once you're already implementingAuthenticationProvider, but doing so throws away whatever built-inUserDetailsServiceimplementations (JDBC, LDAP) would otherwise be reusable — reach for a fully custom, dependency-freeauthenticate()only when the credential shape genuinely isn't username/password (an API key, a signed header). - One misbehaving provider can mask another. With several providers
registered, a bug that makes one throw instead of returning
nullfor a type it doesn't truly own aborts authentication for every other provider too — the manager never gets the chance to try the rest. - Book vs. today: registering the provider no longer needs
WebSecurityConfigurerAdapter. The book plugsCustomAuthenticationProviderin by overridingconfigure(AuthenticationManagerBuilder auth)onWebSecurityConfigurerAdapter— a class deprecated in 5.7 and removed since Spring Security 6.0 / Spring Boot 3. Today, exposing the provider as a plain@Bean/@Componentof typeAuthenticationProvider(orUserDetailsService, orAuthenticationManager) is enough: per the current Spring Boot reference, Spring Boot's security auto-configuration backs off once such a bean exists, andProviderManagerpicks it up automatically — no subclassing, no builder, confirmed via the current official Spring Boot and Spring Security docs.AuthenticationManagerBuilderitself still exists and isn't deprecated as a class; it's just no longer the necessary path for this simple case. - Book vs. today:
DaoAuthenticationProvider's constructor tightened. Unrelated to this book section but relevant to the same chapter's default provider — since Spring Security 6.5,DaoAuthenticationProviderrequires aUserDetailsServicein its constructor; the earlier no-arg constructor andsetUserDetailsService()setter are deprecated (and dropped entirely as of the current 7.x API docs), pushing every default-flow setup toward constructor injection instead of JavaBean-style configuration. - Book vs. today (new capability, not a correction):
AuthenticationgainedtoBuilder()in Spring Security 7.0, returning anAuthentication.Builderthat can mutate credentials/details/principal/authorities and derive a new, authenticated instance from an existing one — something the book's 2020-era contract didn't offer and couldn't have described.