spring-cloud-gateway icon indicating copy to clipboard operation
spring-cloud-gateway copied to clipboard

Error in Spring Cloud GatewayFilter for async calls, on token renewal, using refresh token that gets revoked after single use

Open anuragroy17 opened this issue 1 year ago • 2 comments

I am using Spring Cloud Gateway in authorization code flow and have a filter to authorize every request and add a custom jwt thereafter. The oAuth server provides refresh and access token on login, but the refresh token can be used only once as it's revoked after providing a new refresh and access token. My api gateway is working well when the web client sends requests at a normal pace. However, there is now a race condition when access token has expired and multiple async calls happen. If two or more requests are made from the browser simultaneously, request 1 triggers a refresh with the authorization server. Before request 1 completes, request 2 reaches the gateway and triggers another refresh. The first request will succeed, but the second one will fail because the old refresh token has been revoked. I am using spring-boot v3.3.3 and spring-cloud-gateway v4.1.5 Please note that I can't change the single use refresh token policy in my oauth server

Is there any configuration that can address this issue, or can I make any changes in the filter? Below is my filter, and it gets the error in the authorizeClient method.

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.ClientAuthorizationException;
import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProvider;
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientProviderBuilder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
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.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebSession;
import reactor.core.publisher.Mono;
import com.abc.service.UserService;
import java.time.Duration;

@Component
@Slf4j
public class CustomTokenRelayGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
    private final ReactiveOAuth2AuthorizedClientManager authorizedClientManager;
    
    private final UserService userService;

    public CustomTokenRelayGatewayFilterFactory(final ServerOAuth2AuthorizedClientRepository authorizedClientRepository,
                                                final ReactiveClientRegistrationRepository clientRegistrationRepository,
                                                final UserService userService) {
        super(Object.class);
        userService = userService;
        authorizedClientManager = generateDefaultAuthorizedClientManager(clientRegistrationRepository,
                authorizedClientRepository);
    }

    private ReactiveOAuth2AuthorizedClientManager generateDefaultAuthorizedClientManager(
            final ReactiveClientRegistrationRepository clientRegistrationRepository,
            final ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {
        final Duration tokenClockSkewDuration = Duration.ofSeconds(5);
        final ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider
                = ReactiveOAuth2AuthorizedClientProviderBuilder.builder().authorizationCode()
                .refreshToken(configurer -> configurer.clockSkew(tokenClockSkewDuration))
                .clientCredentials(configurer -> configurer.clockSkew(tokenClockSkewDuration))
                .password(configurer -> configurer.clockSkew(tokenClockSkewDuration)).build();
        final DefaultReactiveOAuth2AuthorizedClientManager defaultAuthorizedClientManager
                = new DefaultReactiveOAuth2AuthorizedClientManager(clientRegistrationRepository,
                authorizedClientRepository);
        defaultAuthorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return defaultAuthorizedClientManager;
    }

    @Override
    public GatewayFilter apply(final Object config) {
        return (exchange, chain) ->
                exchange.getSession().flatMap(mapSession ->
                        exchange.getPrincipal().log("token-relay-filter")
                                .filter(principal -> principal instanceof OAuth2AuthenticationToken)
                                .cast(OAuth2AuthenticationToken.class)
                                .flatMap(this::authorizeClient)
                                .map(auth2AuthenticationToken -> auth2AuthenticationToken.getName())
                                .flatMap(userService::getRoles)
                                .flatMap(userRoles -> this.withBearerAuth(exchange, userRoles))
                                .onErrorResume(ClientAuthorizationException.class,
                                        e -> Mono.defer(() -> exchange.getSession()
                                                .map(WebSession::invalidate))
                                                .map(a -> exchange))
                                .defaultIfEmpty(exchange)
                                .flatMap(chain::filter)

                );
    }

    Mono<ServerWebExchange> withBearerAuth(final ServerWebExchange exchange, final UserRoles userRoles) {
        // add custom jwt token in auth header
    }

    Mono<OAuth2AuthenticationToken> authorizeClient(final OAuth2AuthenticationToken oAuth2AuthenticationToken) {
        final String clientRegistrationId = oAuth2AuthenticationToken.getAuthorizedClientRegistrationId();

        return Mono.defer(() -> authorizedClientManager.authorize(
                        createOAuth2AuthorizeRequest(clientRegistrationId, oAuth2AuthenticationToken)))
                .map(oAuth2AuthorizedClient -> oAuth2AuthenticationToken);
    }
    
    private OAuth2AuthorizeRequest createOAuth2AuthorizeRequest(final String clientRegistrationId,
                                                                final Authentication principal) {
        return OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistrationId).principal(principal).build();
    }
}

anuragroy17 avatar Jan 03 '25 05:01 anuragroy17

Our company have the same issue. Is there any progress or plan for fix it please? Its blocking issue for modern low latency application with high throughput and traffic. Thx.

salopa avatar Aug 08 '25 13:08 salopa

We are currently trying to test a solution, based on this SO comment: https://github.com/spring-projects/spring-security/issues/11461#issuecomment-1184917008

Initial testing shows it is working for our use case.

eddieescoqc avatar Aug 08 '25 19:08 eddieescoqc