I am following the guide here that has the following snippet of code for auditing Spring Security login attempts from Spring Boot Actuator
@Component
public class LoginAttemptsLogger {
@EventListener
public void auditEventHappened(
AuditApplicationEvent auditApplicationEvent) {
AuditEvent auditEvent = auditApplicationEvent.getAuditEvent();
System.out.println("Principal " + auditEvent.getPrincipal()
+ " - " + auditEvent.getType());
WebAuthenticationDetails details =
(WebAuthenticationDetails) auditEvent.getData().get("details");
System.out.println("Remote IP address: "
+ details.getRemoteAddress());
System.out.println(" Session Id: " + details.getSessionId());
}
}
But when I use this code I get the error
java.util.LinkedHashMap cannot be cast to org.springframework.security.web.authentication.WebAuthenticationDetails
I am using a stateless OAuth2 JWT security config using Spring Boot 1.5.10.RELEASE
with Spring Boot Actuator. If I remove the part about the details
then it works fine.
edit: So I just discovered that the value returned by my details is different than the properties of WebAuthenticationDetails
. My details contains grant_type, scope, and username instead of remoteAddress and sessionId needed to cast to WebAuthenticationDetails
. Interestingly when I access the actuator endpoint /auditevents
the value of the details field contains the remoteAddress and sessionId. Hmmm. So this definitely means it's because I'm using OAuth2 but I don't know what exactly is the cause.
edit2: I also just noticed it is only publishing events for the password/client_credentials grant type. If possible I would also like to use this same listener for the refresh_token grant type as well
edit3: Here is my Authorization Server config
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private DataSource dataSource;
@Value("${tokenSigningKey}")
private String tokenSigningKey;
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
CustomJwtAccessTokenConverter accessTokenConverter = new CustomJwtAccessTokenConverter();
accessTokenConverter.setSigningKey(tokenSigningKey);
return accessTokenConverter;
}
@Bean
public TokenStore tokenStore() {
return new CustomJwtJdbcTokenStore(accessTokenConverter(), dataSource);
}
@Bean
@Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
@Bean
public TokenEnhancer tokenEnhancer() {
return new CustomAccessTokenEnhancer();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new CustomPasswordEncoder();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.jdbc(dataSource).passwordEncoder(passwordEncoder());
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
endpoints
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.passwordEncoder(passwordEncoder());
security.checkTokenAccess("isAuthenticated()");
}
}
So, AuthenticationManager
is the one that publishes authentication success events, and it is only used by ResourceOwnerPasswordTokenGranter
. This is why you only see it with one grant type (password) because this is the only TokenGranter
that so happens to authenticate the resource owner.
With the remaining token grants, the authorization server is presented with an authorization code, a refresh token, or just trusts the client credentials. Since no owner is authenticated, no event is published.
It's arguable that the details that are incidentally published by ResourceOwnerPasswordTokenGranter
ought to be something other than a LinkedHashMap
, but I'm thinking that you'll want to do something different anyway since you are more looking for a token event.
There isn't really a good injection point for what you want to do. The model for granting tokens is different than the model for authenticating users. For example, TokenEndpoint
doesn't have access to the HTTP request, which you'd need in order to construct the kind of details object you want.
One thing you could do that is icky is extend AuthorizationServerSecurityConfiguration
to customize how the client authenticationManager
is built. It's not an intended extension point, but it works for me:
Extend AuthorizationServerSecurityConfiguration
public class PublishingAuthorizationServerSecurityConfiguration
extends AuthorizationServerSecurityConfiguration {
@Autowired
AuthenticationEventPublisher authenticationEventPublisher;
@Override
public void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.getSharedObject(AuthenticationManagerBuilder.class)
.authenticationEventPublisher
(authenticationEventBuilder);
}
}
Switch out @EnableAuthorizationServer
for
@Import(
{AuthorizationServerEndpointsConfiguration.class,
PublishingAuthorizationServerSecurityConfiguration.class})
Not great, but it does give me an audit trail for the client auth of each token grant.