spring-cloud / spring-cloud-gateway

An API Gateway built on Spring Framework and Spring Boot providing routing and more.
http://cloud.spring.io
Apache License 2.0
4.54k stars 3.33k forks source link

TokenRelay bug when using different oauth2 client registration #3535

Open rpapeters opened 1 month ago

rpapeters commented 1 month ago

spring-cloud-starter-gateway v4.1.5

When using a different client registration for the TokenRelay filter (like TokenRelay=someClientRegistrationId and not the one used for logging in the user), the Bearer auth header is not set. I think this is because the client used for the TokenRelay does not get an authorizedClient.

Example application security config:

  security:
    oauth2:
      client:
        provider:
          myAuthProvider:
            issuer-uri: ${issuerUri}
            user-name-attribute: name
        registration:
          loginClient:
            provider: myAuthProvider
            authorization-grant-type: authorization_code
            client-id: ${clientId}
            client-secret: ${clientSecret}
            scope: openid,profile,email,offline_access
          resourceClient:
            provider: myAuthProvider
            authorization-grant-type: client_credentials
            client-id: ${clientId}
            client-secret: ${clientSecret}
            scope: /.default
  cloud:
    gateway:
      routes:
        - id: resourceServerRoute
          uri: ${resouceServerUri}
          predicates:
            - Path=/resource/**
          filters:
            - TokenRelay=resourceClient

Suggested solution (inspired by https://docs.spring.io/spring-security/reference/reactive/oauth2/client/authorization-grants.html#_using_the_access_token):

In function TokenRelayGatewayFilterFactory.authorizationRequest add .attribute(ServerWebExchange.class.getName(), exchange) to the builder like so:

    private Mono<OAuth2AuthorizeRequest> authorizationRequest(String defaultClientRegistrationId,
                                                              Authentication principal,
                                                              ServerWebExchange exchange) {
        String clientRegistrationId = defaultClientRegistrationId;
        if (clientRegistrationId == null && principal instanceof OAuth2AuthenticationToken) {
            clientRegistrationId = ((OAuth2AuthenticationToken) principal).getAuthorizedClientRegistrationId();
        }
        return Mono.justOrEmpty(clientRegistrationId).map(OAuth2AuthorizeRequest::withClientRegistrationId)
                .map(builder -> builder.principal(principal).attribute(ServerWebExchange.class.getName(), exchange).build());
    }
rpapeters commented 1 month ago

So I was having multiple moving parts when I was trying to solve this issue, and now looking into it a bit further after a good night sleep the suggested solution in my first post is actually not what solved the issue. The other thing I changed was adding explicit security config and adding the clientCredentials as provider, see code below:

@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {

    @Bean
    public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
            ReactiveClientRegistrationRepository clientRegistrationRepository,
            ServerOAuth2AuthorizedClientRepository authorizedClientRepository) {

        ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
                ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
                        .authorizationCode()
                        .clientCredentials()
                        .build();

        DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
                new DefaultReactiveOAuth2AuthorizedClientManager(
                        clientRegistrationRepository, authorizedClientRepository);
        authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

        return authorizedClientManager;
    }
}