Spring Boot: Logging Failed Logins

In many applications it's important to react to failed logins and other security-critical events, for example to log the failed login attempt to a file or to display a captcha after repeated failures. Spring-based applications come with a simple mechanism to access this kind of information using Application Events.

Spring's Application Events are a variation of the old Observer pattern where an object can subscribe to an Observable to receive notifications. With Application Events, you define a @Component that either implements ApplicationListener or has an @EventListener method that gets the event as a parameter (see below).

There are two event sources available. The first is Spring Security itself which publishes fine-grained events on authentication and authorization. The second one is Spring Boot's Actuator which listens to Spring Security's events and maps them to AuditApplicationEvent instances where each contains an AuditEvent. This is part of Spring Boot's Auditing features.

Listening to AuditApplicationEvent instances is very simple:

@Component
public class MyAuditListener {
    private static final Logger LOG = LoggerFactory.getLogger("security");

    @EventListener
    public void onAuditEvent(AuditApplicationEvent event) {
        AuditEvent auditEvent = event.getAuditEvent();
        LOG.info("type={}, principal={}", auditEvent.getType(), auditEvent.getPrincipal());
    }
}

The AuditEvent has a type property where the following values are relevant for security logging:

  • AUTHENTICATION_SUCCESS (successfully logged in; triggered on each request for stateless applications)
  • AUTHENTICATION_FAILURE (bad credentials etc.)
  • AUTHENTICATION_SWITCH (for user impersonation)
  • AUTHORIZATION_FAILURE (insufficient permissions to access a resource)

The principal property typically contains the user name and there's a data property for more details.

Usually, I would use a switch statement on the type property, but if you prefer, you can use Spring's Expression Language to further restrict the listener method:

@EventListener(condition = "#event.auditEvent.type == 'AUTHENTICATION_FAILURE'")
public void onAuthFailure(AuditApplicationEvent event) {
    AuditEvent auditEvent = event.getAuditEvent();
    LOG.info("authentication failure, user: {}", auditEvent.getPrincipal());
}

social