skip to content

Authentication Events

Spring Security publishes success and failure events you can listen to for auditing, lockout counting or alerting. The natural answer to 'how would you detect and respond to credential stuffing'.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

questions

5

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

open as a page

What are Spring Security authentication events, and what publishes them?

level: juniorimportance: should knowfreq 35%

basics

~10 s

When a login succeeds or fails, Spring Security publishes an application event (like AuthenticationSuccessEvent or a failure event). You can listen to these events to log or audit logins without changing the login code.

open as a page

How would you build a login audit trail using @EventListener handlers for authentication events?

level: middleimportance: should knowfreq 40%

basics

~10 s

Write a Spring bean with @EventListener methods for AuthenticationSuccessEvent and AbstractAuthenticationFailureEvent. Read the username and (for failures) the exception, then persist or log an audit record. No changes to the security config are needed.

open as a page

A team reports authentication events never fire in their app. What are the likely causes and how does the wiring actually work?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Usually the AuthenticationManager has no AuthenticationEventPublisher set — common when you build ProviderManager by hand. Fix it by injecting DefaultAuthenticationEventPublisher (or Boot's autoconfigured one). Also check the failure exception is actually mapped to an event.

open as a page

Beyond authentication, how do you audit authorization decisions with AuthorizationDeniedEvent, and how does that differ from authentication events?

level: principalimportance: should knowfreq 25%

basics

~10 s

Spring Security 6 can publish AuthorizationGrantedEvent and AuthorizationDeniedEvent for access-control decisions. Register a SpringAuthorizationEventPublisher bean, then use @EventListener(AuthorizationDeniedEvent.class) to audit denied access — separate from login success/failure events.

open as a page