How would you build a login audit trail using @EventListener handlers for authentication events?
answer
- @Component + @EventListener on success and base failure
- listen on AbstractAuthenticationFailureEvent to catch all
- getName() ok, never log getCredentials()
- synchronous in caller thread/tx by default -> @Async to decouple
- AuthenticationSuccessEvent vs InteractiveAuthenticationSuccessEvent double-fire
basics
~10 sWrite 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.
solid answer
~40 sCreate an @Component with @EventListener methods, one taking `AuthenticationSuccessEvent` and one taking `AbstractAuthenticationFailureEvent` (the base type catches every failure subtype). From each event read `getAuthentication().getName()` for the principal, and for failures `getException()` for the cause. Enrich with request context — client IP, user agent — via `RequestContextHolder` or by also having the event fire in a servlet request thread. Persist an audit record or emit a metric. Keep the listener fast; if the work is slow or must not block the login, mark it `@Async` (with async enabled) or offload to a queue, and remember listeners run in the caller's thread and transaction by default. This gives a decoupled, provider-agnostic audit trail that also captures programmatic and non-form logins.
code
java · 29 linesimport org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.security.authentication.AuthenticationSuccessEvent;
import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent;
import org.springframework.stereotype.Component;
@Component
public class AuthAuditListener {
private final AuditRepository repo;
public AuthAuditListener(AuditRepository repo) {
this.repo = repo;
}
@Async // requires @EnableAsync; keeps slow audit writes off the login thread
@EventListener
public void onSuccess(AuthenticationSuccessEvent event) {
repo.save(AuditRecord.success(event.getAuthentication().getName()));
}
@Async
@EventListener
public void onFailure(AbstractAuthenticationFailureEvent event) {
repo.save(AuditRecord.failure(
event.getAuthentication().getName(), // attempted principal
event.getException().getClass().getSimpleName())); // never log credentials
}
}go deeper
Write the listener bean and read the username from the event.
Handle both success and the base failure type and avoid logging credentials.
Reason about synchronous execution, transactions, @Async, and the double-fire nuance.
Design the audit pipeline (events vs handlers, async offload, PII/credential hygiene, at-least-once delivery).
## Goal A login audit trail records who authenticated, when, from where, and — for failures — why. Authentication events make this cleanly decoupled: your audit code never touches `SecurityFilterChain`, filters, or providers. ## Listener shape ```java @Component class AuthAuditListener { private final AuditRepository repo; AuthAuditListener(AuditRepository repo) { this.repo = repo; } @EventListener void onSuccess(AuthenticationSuccessEvent e) { repo.save(AuditRecord.success(e.getAuthentication().getName(), clientIp())); } @EventListener void onFailure(AbstractAuthenticationFailureEvent e) { repo.save(AuditRecord.failure( e.getAuthentication().getName(), e.getException().getClass().getSimpleName(), clientIp())); } } ``` Listening on the base `AbstractAuthenticationFailureEvent` catches **all** failure subtypes; if you want per-cause handling, add methods typed to specific subtypes (e.g. `AuthenticationFailureLockedEvent`). ## Reading the principal safely `getAuthentication().getName()` gives the attempted username even on failure (the pre-auth token). Do **not** call `getAuthentication().getCredentials()` and log it — that can leak the raw password. Never log credentials. ## Getting request metadata (IP, user agent) Authentication events don't carry the `HttpServletRequest`. Two options: (1) since the event is published synchronously on the request thread for interactive logins, pull the request from `RequestContextHolder.getRequestAttributes()`; (2) for richer context, prefer Spring Security's own `AuthenticationSuccessHandler`/`AuthenticationFailureHandler` which receive the request directly. Events remain best for mechanism-agnostic auditing. ## Threading and transactions — the key gotchas - By default `@EventListener` runs **synchronously in the publisher's thread**, inside whatever transaction is active. A slow or failing listener can therefore slow down or break the login flow (an exception thrown by a synchronous listener propagates to the caller). - To decouple, annotate with `@Async` (needs `@EnableAsync`) or push to a message queue. Async listeners lose the request thread's `RequestContextHolder`/`SecurityContext` unless propagated. - If persisting via JPA, be aware the listener may run without an active transaction depending on the caller — annotate the audit save method with `@Transactional(propagation = REQUIRES_NEW)` for a self-contained write. ## Success event nuance `AuthenticationSuccessEvent` fires from the `AuthenticationManager`. For **form/interactive** logins the filter also publishes `InteractiveAuthenticationSuccessEvent` later. If you listen to both you'll audit interactive logins twice — pick the one matching your intent (`AuthenticationSuccessEvent` covers all authentications including programmatic; `InteractiveAuthenticationSuccessEvent` is only interactive filter-based logins). ## When to use events vs handlers - **Events (@EventListener):** cross-cutting audit/metrics that should fire for every authentication path. - **Success/Failure handlers:** when you need the HTTP request/response (redirects, custom JSON errors, cookies).
- Your @EventListener throws — what happens to the login?For a synchronous listener the exception propagates back to the AuthenticationManager caller and can fail the authentication. Make audit listeners resilient (catch/log) or run them @Async so they can't break login.
- How do you capture the client IP in the listener?Pull the request from RequestContextHolder while on the request thread, or better, use an AuthenticationFailure/SuccessHandler which receives HttpServletRequest directly. Async listeners lose the request context.
saying these in an interview costs you the question
- Logging getAuthentication().getCredentials() (leaks the password)
- Assuming listeners are always async and can't affect the login
- Listening to both AuthenticationSuccessEvent and InteractiveAuthenticationSuccessEvent and double-counting interactive logins