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

Upvotes: 2

Views: 1473

Answers (2)

ingvar
ingvar

Reputation: 4375

Non-reactive implementation as of Spring security 6.4.x:

public class DelegatingOAuth2AuthorizedClientManager implements OAuth2AuthorizedClientManager {

    private final OAuth2AuthorizedClientManager delegate;

    public DelegatingOAuth2AuthorizedClientManager(OAuth2AuthorizedClientManager delegate) {
        this.delegate = delegate;
    }

    @Override
    public OAuth2AuthorizedClient authorize(OAuth2AuthorizeRequest authorizeRequest) {
        try {
            return delegate.authorize(authorizeRequest);
        } catch (ClientAuthorizationException ex) {
            throw new ClientAuthorizationRequiredException(authorizeRequest.getClientRegistrationId());
        }
    }
}

Bean registration:

@Bean
public OAuth2AuthorizedClientManager getOAuth2AuthorizedClientManager(
        ClientRegistrationRepository clientRegistrationRepository,
        OAuth2AuthorizedClientRepository authorizedClientRepository) {

    final var manager = new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);

    return new DelegatingOAuth2AuthorizedClientManager(manager);
}

Upvotes: 0

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

Upvotes: 1

Related Questions