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