spring-projects / spring-security

Spring Security
http://spring.io/projects/spring-security
Apache License 2.0
8.78k stars 5.89k forks source link

Add support for requesting protected resources with RestClient via a ServletBearerExchangeFilterFunction or equivalent #15820

Open azizabah opened 1 month ago

azizabah commented 1 month ago

Expected Behavior It would be nice if the RestClient supported an equivalent of ServletBearerExchangeFilterFunction. This would allow us to easily grab a user's bearer token and pass that on to subsequent client calls without having to explicitly grab the header and token etc.

Current Behavior Currently I can implement this very easily for a WebClient like this:

@Bean
  public WebClient profileServiceWebClient(WebClient.Builder webClientBuilder) {
    return webClientBuilder.filter(new ServletBearerExchangeFilterFunction()).baseUrl(profileServiceBaseUrl)
        .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).build();
  }

As far as I know there is no equivalent implementation for the new RestClient. Context This issue is seen as a pretty large blocker for code bases that have to pull a dependency on Spring Boot Starter Webflux (or equivalent) to use WebClient when they are not using a reactive code base. It would be much more preferable to not have to pull that dependency and not have to use reactive code inside a non-reactive code base.

ch4mpy commented 3 days ago

What about making the OAuth2ClientHttpRequestInterceptor more versatile? I mean behave as a BearerClientHttpRequestInterceptor. As a side note, the same principle would apply to ServletOAuth2AuthorizedClientExchangeFilterFunction and ServletBearerExchangeFilterFunction which could be merged into a single class (same for Server equivalents).

As I understand, these ClientHttpRequestInterceptor & ExchangeFilterFunction serve two purposes:

  1. get a Bearer from somewhere: an OAuth2 client registration or the Authentication in the security context of an oauth2ResourceServer.
  2. optionally handle authorization failure with a (Reactive)OAuth2AuthorizationFailureHandler

Maybe could the framework provide a few implementations for each of these two strategies and make it configurable on a single interceptor / filter?

Such ServletBearerExchangeFilterFunction, ServerBearerExchangeFilterFunction, and BearerHttpRequestInterceptor would be configurable with a minimum of:

This would require the framework user to provide much less code when needing an exotic means to get the Bearer token or to handle REST requests authorization failures: instead of writing the all filter / interceptor, with the imbrication of Bearer acquisition and failure handling he'd provide just what he needs to be customized.

We could also expect Spring Boot to auto-configure named interceptor / filter beans with the chosen strategies based on application properties.

WebClient / RestClient named beans could be exposed and pre-configured with such filter / interceptors. Something like:

spring:
  rest-clients:
    client-a:
      base-url: http://localhost:8081
      bearer:
        registration: registration-a
        failure-handler: remove-authorized-client
    client-b:
      base-url: http://localhost:8082
      bearer:
        forward: true
        failure-handler: no-op
@RestController
@RequiredArgsConstructor
static class MyRestController {
  private final RestClient clientA; // pre-configured with client-a properties above
  private final RestClient clientB; // pre-configured with client-b properties above
}

Maybe, could Bearer forwarding be the default on resource servers when the spring.rest-clients.{clientId}.bearer.registration is empty. no-op could probably be the default for Bearer forwarding and remove-authorized-client the default when a client registration is used.

To go even further, we could dream of the recent @HttExchange service proxy factory being involved in the picture:

spring:
  rest-proxies:
    service-a:
      client-id: client-a
      http-exchange-interface: com.c4-soft.RestServiceA
@HttpExchange
public interface RestServiceA {
  @GetExchange(url = "/{realm}/users/count")
  Long getTotalUsersCount(@PathVariable(name = "realm") String realm);
}

This would expose a serviceA bean of type RestServiceA, with an implementation using the clientA RestClient bean pre-configured above with a base path (that is likely to change across deployments) and Bearer authorization!

We'd need no more than the following for using it:

@RestController
@RequiredArgsConstructor
static class MyRestController {
  private final RestServiceA serviceA;

  @GetMapping("/users-count")
  Long getUsersCount(@RequestParam String realm) {
    return serviceA.getTotalUsersCount(realm);
  }
}

This would provide something close to what Spring Cloud did for @FeignClient, and I'm currently working on that in this starter. Of course, I'd love that to be in the core framework (and reduce the number of features I have to maintain ;).

sjohnr commented 3 days ago

@ch4mpy thanks for providing your thoughts! Most of the features you're sketching out here would be applicable to Spring Boot, not Spring Security. You are welcome to open an issue there. I agree that configuration properties are nice, but this style of feature is akin to programming with yaml, which I don't believe is specifically the purpose of Spring Boot's auto-configuration features. You would need to clarify that with the Boot team however, as I can't speak for them. Of course, it's fine for a 3rd party library such as yours to provide them though.

Maybe could the framework provide a few implementations for each of these two strategies?

Let's stay focused on this ticket which is aimed at providing a ClientHttpRequestInterceptor for OAuth2 Resource Server. If you have other ideas, please open a new issue with those.

ch4mpy commented 3 days ago

@sjohnr I know by experience that Spring Boot team will decline security features if Spring Security team doesn't see their value (and also that most of Boot code related to security is written by Spring Security team members). So, I'll save everybody time and keep working on my lib.

As a side note, my intention is to "limit programming static configuration in application code", not "programming with YAML".

There is nothing creative in what I list, this is mostly what Spring Cloud did for @FeignClient. And in my daily work implementing REST backends with OAuth2 micro-services, hiding configuration details with Boot & YAML: