Search code examples
javaspringspring-bootspring-securityspring-boot-actuator

Spring OAuth2 java.util.LinkedHashMap cannot be cast to org.springframework.security.web.authentication.WebAuthenticationDetails


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()");
    }

}

Solution

  • 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:

    1. 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);
          }
      }
      
    2. 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.