What are the AbstractAuthenticationFailureEvent subtypes, and how does DefaultAuthenticationEventPublisher decide which one to fire?
answer
- exception class -> event class map
- BadCredentials/Locked/Disabled/Expired/CredentialsExpired
- unmapped custom exception = no event
- setAdditionalExceptionMappings + setDefaultAuthenticationFailureEvent
- UsernameNotFound folds into BadCredentials (anti-enumeration)
basics
~10 sEach kind of login failure has its own event, like AuthenticationFailureBadCredentialsEvent or AuthenticationFailureLockedEvent. DefaultAuthenticationEventPublisher maps the thrown exception type to the matching event. Unmapped exceptions fire no event unless you configure a default.
solid answer
~30 sOn failure, `ProviderManager` passes the `AuthenticationException` to `DefaultAuthenticationEventPublisher`, which holds an internal map from exception class to failure-event class. `BadCredentialsException` → `AuthenticationFailureBadCredentialsEvent`, `LockedException` → `...LockedEvent`, `DisabledException` → `...DisabledEvent`, `AccountExpiredException` → `...ExpiredEvent`, `CredentialsExpiredException` → `...CredentialsExpiredEvent`, `ProviderNotFoundException` → `...ProviderNotFoundEvent`, plus a couple more. It reflectively constructs the mapped event. Crucially, if the thrown exception type isn't in the map (e.g. a custom `AuthenticationException` subclass), **no event is fired** — unless you register additional mappings via `setAdditionalExceptionMappings(...)` or set a catch-all with `setDefaultAuthenticationFailureEvent(...)`. This mapping-by-type behavior is a common source of "my custom failure never triggers a listener" bugs.
code
java · 17 linesimport org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
@Bean
AuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher delegate) {
DefaultAuthenticationEventPublisher publisher =
new DefaultAuthenticationEventPublisher(delegate);
// custom exception -> custom event (event needs (Authentication, AuthenticationException) ctor)
publisher.setAdditionalExceptionMappings(
java.util.Map.of(MfaRequiredException.class, MfaRequiredFailureEvent.class));
// catch-all so no failure is ever silently dropped
publisher.setDefaultAuthenticationFailureEvent(GenericAuthenticationFailureEvent.class);
return publisher;
}go deeper
Know several failure event names exist for different failure causes.
Explain the type-to-event map and that unmapped exceptions fire nothing.
Know both extension hooks and the anti-enumeration mapping of UsernameNotFound.
Reason about designing a failure-event taxonomy for audit/lockout and the reflection/constructor contract.
## The failure event hierarchy All login-failure events extend the abstract base **`AbstractAuthenticationFailureEvent`** (package `org.springframework.security.authentication.event`), which itself extends `AbstractAuthenticationEvent`. The base exposes `getException()` (the `AuthenticationException`) in addition to `getAuthentication()`. The standard concrete subtypes and the exceptions they correspond to: | Exception thrown | Event fired | |---|---| | `BadCredentialsException` | `AuthenticationFailureBadCredentialsEvent` | | `UsernameNotFoundException` | `AuthenticationFailureBadCredentialsEvent` (mapped to the same, to avoid user enumeration) | | `LockedException` | `AuthenticationFailureLockedEvent` | | `DisabledException` | `AuthenticationFailureDisabledEvent` | | `AccountExpiredException` | `AuthenticationFailureExpiredEvent` | | `CredentialsExpiredException` | `AuthenticationFailureCredentialsExpiredEvent` | | `ProviderNotFoundException` | `AuthenticationFailureProviderNotFoundEvent` | | `AuthenticationServiceException` | `AuthenticationFailureServiceExceptionEvent` | ## How the publisher chooses `DefaultAuthenticationEventPublisher` builds, in its constructor, a `Map<Class<? extends AuthenticationException>, Constructor<? extends AbstractAuthenticationFailureEvent>>`. When `publishAuthenticationFailure(exception, authentication)` is called it: 1. Looks up the **exact class** of the exception in the map (it uses the concrete class, not `instanceof` walking the hierarchy in the simple case). 2. If found, reflectively invokes the mapped event's constructor with `(authentication, exception)` and publishes it. 3. If **not found**, it consults an optional `defaultAuthenticationFailureEventConstructor`; if that too is null, it publishes **nothing** (and logs at debug that no event was mapped). ## Extending the mapping Two hooks: - **`setAdditionalExceptionMappings(Properties)` / the `Map` overload** — register your own `CustomException → CustomFailureEvent`. The custom event must have a `(Authentication, AuthenticationException)` constructor. - **`setDefaultAuthenticationFailureEvent(Class<? extends AbstractAuthenticationFailureEvent>)`** — a catch-all so unmapped exceptions still produce an event. ```java @Bean AuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher delegate) { var publisher = new DefaultAuthenticationEventPublisher(delegate); publisher.setAdditionalExceptionMappings( Map.of(MfaRequiredException.class, MfaRequiredFailureEvent.class)); publisher.setDefaultAuthenticationFailureEvent(GenericAuthenticationFailureEvent.class); return publisher; } ``` ## Gotchas - **Custom `AuthenticationException` subtypes fire no event by default.** This is the #1 surprise — teams throw a bespoke exception and their audit listener silently never runs. - **`UsernameNotFoundException` maps to the *BadCredentials* event**, deliberately, so failure auditing can't distinguish "no such user" from "wrong password" — an anti-enumeration measure. (Note: `DaoAuthenticationProvider` also has `hideUserNotFoundExceptions`, which converts the exception itself to `BadCredentialsException` even earlier.) - Registering a custom `AuthenticationEventPublisher` bean **replaces** the auto-configured one; make sure it still delegates to the `ApplicationEventPublisher` or all events vanish. - The mapped event needs the standard two-arg constructor, or reflection fails at startup. ## When to use Map custom exceptions when your provider throws domain-specific failures (MFA required, IP blocked) and you want dedicated listeners; use the default catch-all when you just want "something fired for every failure" for a metrics counter.
- You threw a custom AuthenticationException and your @EventListener never runs. Why?DefaultAuthenticationEventPublisher maps by exception type; your class isn't in its map, so no event is published. Register it via setAdditionalExceptionMappings or set a defaultAuthenticationFailureEvent catch-all.
- Why does UsernameNotFoundException surface as a BadCredentials failure event?To prevent username enumeration — auditing and error responses shouldn't reveal whether the user exists versus the password being wrong.
saying these in an interview costs you the question
- Claiming every AuthenticationException automatically produces a matching event
- Thinking you can subclass the exception and get an event 'for free' without mapping
- Confusing hideUserNotFoundExceptions (provider setting) with the publisher's exception-to-event mapping