Search code examples
springspring-securityspring-security-oauth2

How do I add a custom OAuth2AuthorizationFailureHandler to the DefaultOAuth2AuthorizedClientManager?


I'm using spring-security, and I'm trying to add my own OAuth2AuthorizationFailureHandler to the DefaultOAuth2AuthorizedClientManager, but I can't find a way to do so. It doesn't appear to be a Bean; it looks like it's instantiated in OAuth2ClientConfiguration.OAuth2ClientWebMvcSecurityConfiguration.addArgumentResolvers().

This github issue looks like it should have enabled me to do this, but I still can't figure out how: https://github.com/spring-projects/spring-security/issues/7583

I read the documentation added in https://github.com/spring-projects/spring-security/commit/2dd40c7de5de72b8f18a51c490d640619c5a6301, and I'm still stumped.

The reason I want to add a failure handler is I want to handle the OAuth2AuthorizationException that's thrown by RefreshTokenOAuth2AuthorizedClientProvider.authorize() when a refresh token is expired. Specifically, when a refresh token is expired, I want to respond with HTTP 403 instead of HTTP 500.


Solution

  • I figured out how to customize the failure handler of the DefaultOAuth2AuthorizedClientManager, but it doesn't actually achieve my end goal of gracefully handling the OAuth2AuthorizationException that is thrown by the RegisteredOAuth2ClientArgumentResolver when trying to use an expired refresh token to retrieve a new access token from the auth server.

    To handle the OAuth2AuthorizationException, create a @ControllerAdvice class with an @ExceptionHandler(OAuth2AuthorizationException.class)-annotated method that does whatever you want. Here's what I did:

    @ControllerAdvice
    public class GlobalControllerAdvice {
    
      /**
       * spring-security-oauth2 automatically refreshes the access token while resolving
       * \@RegisteredOAuth2AuthorizedClient-annotated parameters to @RequestMapping methods.  When it
       * fails to refresh an OAuth2 access token because the refresh token is expired,
       * RefreshTokenOAuth2AuthorizedClientProvider.authorize() throws an OAuth2AuthorizationException.
       * If we didn't handle it here, we'd respond with a HTTP 500, and that's no good.  Instead, we
       * respond with HTTP 403 so that the UI can log itself out.
       */
      @ExceptionHandler(OAuth2AuthorizationException.class)
      ResponseEntity<?> handleHttpStatusCodeException(OAuth2AuthorizationException e) {
        return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
      }
    
    }
    

    If for some reason you want to customize the DefaultOAuth2AuthorizedClientManager failure handler, you probably need to create a whole new OAuth2AuthorizedClientArgumentResolver bean (shown below) and register it via a WebMvcConfigurer (not shown below):

    @Bean
    @Order(0)
    public OAuth2AuthorizedClientArgumentResolver oAuth2AuthorizedClientArgumentResolver(OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager) {
      final OAuth2AuthorizedClientArgumentResolver oAuth2AuthorizedClientArgumentResolver = new OAuth2AuthorizedClientArgumentResolver(oAuth2AuthorizedClientManager);
      return oAuth2AuthorizedClientArgumentResolver;
    }
    
    @Bean
    public OAuth2AuthorizedClientManager oAuth2AuthorizedClientManager(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientRepository authorizedClientRepository) {
      final DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
          clientRegistrationRepository, authorizedClientRepository);
    
      authorizedClientManager.setAuthorizationFailureHandler(new OAuth2AuthorizationFailureHandler() {
        @Override
        public void onAuthorizationFailure(OAuth2AuthorizationException e,
            Authentication authentication, Map<String, Object> map) {
          // Handle auth failure here
        }
      });
    
      return authorizedClientManager;
    }