spring-securityoauth-2.0spring-security-oauth2spring-cloud-gatewayrefresh-token

Spring Cloud Gateway Getting a 500 Exception while trying to refresh_token using expired access_token and refresh_token


I have a secured Spring Cloud Gateway application using ServerHttpSecurity.oauth2Login() that can successfully renew expired access tokens using the refresh token. However, when the refresh token also expires and the application tries to renew the access token with it, I get a 500 Internal Server Error [seems to be caused by a 400 Bad Request error just before it] with the following exception:

org.springframework.security.oauth2.client.ClientAuthorizationException: [invalid_grant] Token is not active
    at org.springframework.security.oauth2.client.RefreshTokenReactiveOAuth2AuthorizedClientProvider.lambda$authorize$0(RefreshTokenReactiveOAuth2AuthorizedClientProvider.java:97) ~[spring-security-oauth2-client-5.4.1.jar:5.4.1]

Full logs here: https://github.com/spring-projects/spring-security/files/8319348/logs.txt

Only if I re-issue the request (refresh browser with the call to the secured endpoint), I will get redirected to the login page (desired behavior).

While debugging, I noticed that re-issuing the request after the 500 Internal Server Error under the hood results in the following exception:

org.springframework.security.oauth2.client.ClientAuthorizationRequiredException: [client_authorization_required] Authorization required for Client Registration Id: <client-id>.

and that is probably what causes the redirect to the login page.

Request execution details here

My question: Can I avoid getting the 500 Internal Server Error and instead be redirected to the login page? If yes, how can I accomplish that?

Environment details Spring Boot: 2.4.0 Spring Cloud: 2020.0.0 Spring Security: 5.4.1


Solution

  • The solution was to catch the 500 caused while refreshing a token and then initiating a new authorization flow, using the next classes:

    import org.springframework.security.oauth2.client.*;
    import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
    import org.springframework.security.oauth2.client.web.DefaultReactiveOAuth2AuthorizedClientManager;
    import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
    import org.springframework.util.Assert;
    import reactor.core.publisher.Mono;
    
    /**
     * A delegating implementation of ReactiveOAuth2AuthorizedClientManager to help deal with a 500 Internal Server Error
     * that is a result of an expired access token. With ReactiveOAuth2AuthorizedClientManagerCustom, we manage to redirect
     * to the login page instead of returning a 500 Internal Server Error to the user/client.
     */
    public class ReactiveOAuth2AuthorizedClientManagerCustom implements ReactiveOAuth2AuthorizedClientManager {
    
        private final ReactiveClientRegistrationRepository clientRegistrationRepository;
        private final ServerOAuth2AuthorizedClientRepository authorizedClientRepository;
        private final ReactiveOAuth2AuthorizedClientManager authorizedClientManager;
    
        public ReactiveOAuth2AuthorizedClientManagerCustom(ReactiveClientRegistrationRepository clientRegistrationRepository,
                                                           ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
            this.clientRegistrationRepository = clientRegistrationRepository;
            this.authorizedClientRepository = authorizedClientRepository;
            this.authorizedClientManager = new DefaultReactiveOAuth2AuthorizedClientManager(
                    this.clientRegistrationRepository, this.authorizedClientRepository
            );
        }
    
        public Mono<OAuth2AuthorizedClient> authorize(OAuth2AuthorizeRequest authorizeRequest) {
            Assert.notNull(authorizeRequest.getClientRegistrationId(), "Client registration id cannot be null");
    
            return this.authorizedClientManager.authorize(authorizeRequest)
                    // The token has expired, therefore we initiate a new grant flow
                    .onErrorMap(
                            ClientAuthorizationException.class,
                            error -> new ClientAuthorizationRequiredException(authorizeRequest.getClientRegistrationId())
                    );
        }
    }
    

    And then adding the next @Bean

     public ReactiveOAuth2AuthorizedClientManagerCustomConfig(ReactiveClientRegistrationRepository clientRegistrationRepository,
                                                                 ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
            this.clientRegistrationRepository = clientRegistrationRepository;
            this.authorizedClientRepository = authorizedClientRepository;
        }
    
        @Bean
        @Primary
        ReactiveOAuth2AuthorizedClientManager authorizedClientManager() {
            return new ReactiveOAuth2AuthorizedClientManagerCustom(
                    this.clientRegistrationRepository, this.authorizedClientRepository
            );
        }