We have seen in a previous topic what events are and how they work. Spring Security provides a number of built-in events that are published in relation to authentication or authorization. This topic will go into more detail about these events.
Spring security events
Both the authentication and the authorization processes can fail due to a variety of reasons. For example, the user might provide the wrong credentials, or the user might not have the necessary permissions to access a resource. In this case, as well as in the case of a successful authentication or authorization, Spring Security publishes different events. Other parts of an application can listen to these events and do some other work.
The names of these events reflect the processes they relate to and their outcomes.
For example, AuthenticationFailureBadCredentialsEvent is published when the authentication process fails due to bad credentials. Similarly, AuthenticationSuccessEvent is published when the authentication process succeeds.
For the authorization process, we have AuthorizationDeniedEvent and AuthorizationGrantedEvent for when the authorization process fails or succeeds, respectively.
The events listed here are just a few of the events that Spring Security provides.
Observe how the hierarchy of events clearly separates the authentication events from the authorization events, while preserving the possible outcomes. The next diagrams try to show this.
Authentication events
Success and failure events for authentication.
Because an authentication failure can occur due to a variety of reasons, Spring Security provides a number of events that are subclasses of AbstractAuthenticationFailureEvent and represent the different reasons for the failure.
Some of the covered reasons are bad credentials, credentials expired, account disabled, account expired, and account locked.
Failure events for authentication.
Authorization events
Success and failure events for authorization
In the case of authorization, there are only two possible outcomes, authorization is either granted or denied. These are reflected in the AuthorizationGrantedEvent and AuthorizationDeniedEvent classes, respectively.
All events are, either directly or indirectly, inheriting from the abstract class ApplicationEvent.
Now that we have seen some of the security events Spring provides, let's see how we can use them in an application.
Listening to authentication events
Suppose we want to log all authentication attempts, either successful or not. To make things more interesting, we will log the time the attempt was made together with other potentially useful information the events provide.
Spring Framework already provides the necessary events for this, AuthenticationSuccessEventand AbstractAuthenticationFailureEvent.
We now need to create the listeners that will handle these events and log the necessary information. Will create listeners by using the convenient @EventListener annotation.
package com.example.test
import org.springframework.context.event.EventListener
import org.springframework.security.authentication.event.AbstractAuthenticationEvent
import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent
import org.springframework.security.authentication.event.AuthenticationSuccessEvent
import org.springframework.stereotype.Component
import java.time.Instant
import java.time.LocalDateTime
import java.util.TimeZone
@Component
class AuthenticationEvents {
@EventListener
fun onSuccess(successEvent: AuthenticationSuccessEvent) {
log(successEvent)
}
@EventListener
fun onFailure(failureEvent: AbstractAuthenticationFailureEvent) {
log(failureEvent)
}
private fun log(event: AbstractAuthenticationEvent) {
val localDateTime = toLocalDateTime(event.timestamp)
when (event) {
is AbstractAuthenticationFailureEvent -> {
println("Failed authentication attempt at: $localDateTime")
println("Exception: ${event.exception}")
}
else -> {
println("Successful authentication attempt at: $localDateTime")
}
}
println("User: ${event.authentication.name}")
println("Authorities: ${event.authentication.authorities}")
println("Details: ${event.authentication.details}")
println("Credentials: ${event.authentication.credentials}")
println("Principal: ${event.authentication.principal}")
println("Is authenticated: ${event.authentication.isAuthenticated}")
}
private fun toLocalDateTime(timestamp: Long): LocalDateTime {
return LocalDateTime.ofInstant(
Instant.ofEpochSecond(timestamp / 1000), TimeZone.getDefault().toZoneId()
)
}
}Every successful authentication attempt will result in calling the onSuccess() method, while every failed attempt, regardless of the reason, will result in calling the onFailure() method.
Looking inside the event
The authentication events provide various information, as we can see in the log() method. This method takes as a parameter the event in question, which has the type AbstractAuthenticationEvent. This class is the parent class of both AuthenticationSuccessEvent and AbstractAuthenticationFailureEvent.
The event.getAuthentication() gives us more information about the authentication data of the respective event. Note that we need to differentiate between the failure event and the success event so that we can log information accordingly. For instance, for the failure event, Spring security gives us the convenient .getException() method that we can call on an object of type AbstractAuthenticationFailureEvent.
Running the application and attempting a login would result in the following data being logged.
For the successful attempt:
Successful authentication attempt at: 2023-10-24T09:57:53
User: user
Authorities: []
Details: WebAuthenticationDetails
[RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=D2EEF909D615F01DE3947924ECC49A2B]
Credentials: null
Principal: org.springframework.security.core.userdetails.User
[Username=user, Password=[PROTECTED], Enabled=true, AccountNonExpired=true,
credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[]]
Is authenticated: trueFor the failed attempt, in this case, because of a wrong password.
Failed authentication attempt at: 2023-10-24T09:59:40
User: user
Authorities: []
Details: WebAuthenticationDetails
[RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=AE995D4084AD82C430EC62872CB35635]
Credentials: 3433a0ee-aa89-4727-b661-b19c2f39d58
Principal: user
Is authenticated: false
Exception: org.springframework.security.authentication.BadCredentialsException: Bad credentialsPublishing authentication events
By default, Spring Security uses a bean of type org.springframework.security.authentication.DefaultAuthenticationEventPublisher called authenticationEventPublisher. This is the bean responsible for publishing authentication events. The bean is automatically created for us when the dependency spring-boot-starter-security is added to the pom.xml file.
We can also create the bean programmatically, as shown below:
@Bean
fun authenticationEventPublisher(applicationEventPublisher: ApplicationEventPublisher): AuthenticationEventPublisher {
return DefaultAuthenticationEventPublisher(applicationEventPublisher)
}Note the type of the bean, AuthenticationEventPublisher, which is a Java interface, and the actual implementation, the DefaultAuthenticationEventPublisher, which is a Java class implementing the interface.
Conclusion
In this topic, we explored how Spring models the security events pertaining to authentication and authorization as Java classes and saw a practical example of how to listen for authentication events, either successful or unsuccessful. Additionally, we took a peek inside such events to see what data is available.