Search code examples
spring-bootspring-securityspring-oauth2spring-boot-oauth2.1

use spring-boot-starter-oauth2-client to pass the token to Spring Framework release 6 HttpInterface @HttpExchange @GetExchange


I am trying to use spring-boot-starter-oauth2-client with the new Spring Framework release 6 HttpInterface

@Configuration
public class RestClientConfig {

    @Bean
    public RestClient restClient(OAuth2AuthorizedClientManager authorizedClientManager, RestClient.Builder restClientBuilder) {
        OAuth2ClientHttpRequestInterceptor interceptor = new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);
        return restClientBuilder
                .requestInterceptor(interceptor)
                .build();
    }

}
@RestController
public class LessonsController {

    private final RestClient restClient;

    public LessonsController(RestClient restClient) {
        this.restClient = restClient;
    }

    @GetMapping("/lessons")
    public String fetchLessons() {
        return restClient.get()
                .uri("https://someserver.om/someprotectedresource")
                .attributes(clientRegistrationId("my-client"))
                .retrieve()
                .body(String.class);
    }
}
spring:
  application:
    name: client-application
  security:
    oauth2:
      client:
        registration:
          my-client:
            provider: my-provider
            client-id: ididid
            client-secret: secretsecret
            authorization-grant-type: client_credentials
            scope: download
        provider:
          my-provider:
            token-uri: https://provider.com/token

The above is working. We have confirmation from the token provider we got the token, as well as from the resource server we got the resource, passing the token. Both two steps are working fine, happy.

https://www.youtube.com/watch?v=aR580OCEp7w We now would like to do the same, with the new Spring Framework release 6 HttpInterface

When doing this:

@Configuration
public class UserClientConfig {

    private final RestClient restClient;

    public UserClientConfig(OAuth2AuthorizedClientManager authorizedClientManager, RestClient.Builder restClientBuilder) {
        OAuth2ClientHttpRequestInterceptor interceptor = new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);
        this.restClient = restClientBuilder
                .requestInterceptor(interceptor)
                .baseUrl("https://host.com")
                .build();
    }

    @Bean
    public UserClient userClient() {
        RestClientAdapter adapter = RestClientAdapter.create(restClient);
        return HttpServiceProxyFactory.builderFor(adapter)
                .build()
                .createClient(UserClient.class);
    }

}
@HttpExchange(
        url = "/v1",
        accept = MediaType.APPLICATION_JSON_VALUE)
public interface UserClient {

    @GetExchange("/protectedresource/full")
    public User getUserById(@RequestParam Map<String, String> key value);

}
    @GetMapping("/lessons")
    public User fetchLessons() {
        return userClient.getUserById(Map.of("foo", "bar"));
    }

When using HttpInterface, this would not work. The token is not fetched in the first place. Maybe because of the lack of .attributes(clientRegistrationId("id")) for @HttpExchange @GetExchange, but not sure.

Question: how to combine Http Interface with spring-boot-starter-oauth2-client token?


Solution

  • You should setClientRegistrationIdResolver on your OAuth2ClientHttpRequestInterceptor instance:

    final var interceptor = new OAuth2ClientHttpRequestInterceptor(authorizedClientManager);
    // Whatever the request, always resolve the my-client registration defined in application.yml
    interceptor.setClientRegistrationIdResolver((HttpRequest request) -> "my-client");
    

    This is what I do in a starter of mine which helps auto-configuring RestClient and WebClient beans with various request authorization mechanisms (Basic, Bearer, and API key) and HTTP proxy, using just application properties. In your case that would give:

    spring:
      application:
        name: client-application
      security:
        oauth2:
          client:
            registration:
              my-registration:
                provider: my-provider
                client-id: ididid
                client-secret: secretsecret
                authorization-grant-type: client_credentials
                scope: download
            provider:
              my-provider:
                token-uri: https://provider.com/token
    com:
      c4-soft:
        springaddons:
          rest:
            client:
              user-client:
                base-url: https://host.com
                authorization:
                  oauth2:
                    oauth2-registration-id: my-registration
    
    @Configuration
    public class RestConfiguration {
    
      @Bean
      // userClient is auto-configured with OAuth2 security by spring-addons-starter-rest
      // using the com.c4-soft.springaddons.rest.client.user-client properties above (the bean name "userClient" is the camelCase transformation of the "user-client" key in application properties)
      UserApi userApi(RestClient userClient) throws Exception {
        return new RestClientHttpExchangeProxyFactoryBean<>(UserApi.class, userClient).getObject();
      }
    }
    
    @RestController
    @RequiredArgsConstructor
    public class LessonsController {
      // the @HttpExchange proxy defined above (or auto-configured RestClient beans)
      // can then be auto-wired in Spring components
      private final UserApi userApi;
    }
    

    Bootiful isn't it?

    Note that for clarification purposes, I renamed:

    • the OAuth2 client registration from my-client to my-registration
    • the @HttpExchange from UserClient to UserApi (userClient being used as name for the auto-configured RestClient @Bean internally used by the the @HttpExchange proxy)