skip to content

What are the AbstractAuthenticationFailureEvent subtypes, and how does DefaultAuthenticationEventPublisher decide which one to fire?

level: middleimportance: must knowfreq 45%

answer

  1. exception class -> event class map
  2. BadCredentials/Locked/Disabled/Expired/CredentialsExpired
  3. unmapped custom exception = no event
  4. setAdditionalExceptionMappings + setDefaultAuthenticationFailureEvent
  5. UsernameNotFound folds into BadCredentials (anti-enumeration)

basics

~10 s

Each 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 s

On 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 lines
java
import 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

for a junior

Know several failure event names exist for different failure causes.

for a middle

Explain the type-to-event map and that unmapped exceptions fire nothing.

for a senior

Know both extension hooks and the anti-enumeration mapping of UsernameNotFound.

for a principal

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

context