Spring Security 5.1 - Get Token for Client Credentials Flow with WebClient

Darren Forsythe :

I am attempting to get a bearer token via a webclient with the following setup for an integration test of a secured resource server in a servlet application.

spring:
  security:
    oauth2:
      client:
        registration:
          idp:
            clientId: id
            clientSecret: secret
            authorization-grant-type: client_credentials
            scope: read
        provider:
          idp:
            authorization-uri: myidp/authorization.oauth2
            token-uri: myidp/token.oauth2
            user-info-uri: myidp/userinfo.openid
            user-name-attribute: name

And beans,

    @Bean
    WebClient webClient(ClientRegistrationRepository clientRegistrations,
            OAuth2AuthorizedClientRepository authorizedClients) {
        ServletOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServletOAuth2AuthorizedClientExchangeFilterFunction(
                clientRegistrations, authorizedClients);
        // (optional) explicitly opt into using the oauth2Login to provide an access token implicitly
        // oauth.setDefaultOAuth2AuthorizedClient(true);
        // (optional) set a default ClientRegistration.registrationId
        // oauth.setDefaultClientRegistrationId("client-registration-id");
        return WebClient.builder().apply(oauth.oauth2Configuration()).build();
    }

and autowiring the webclient to a test and calling it like so,

webClient.get().uri("http://localhost:" + port + "/web/it")
                .attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId("idp")).retrieve()
                .bodyToMono(String.class).block();

It is my assumption the exchange function would either get an access token if available, or do a call to get a new one from the IDP. However it will always fail as the HttpSessionOAuth2AuthorizedClientRepository as the HttpServletRequest is null.

With the overall test looking like, this feeds into an autoconfiguration to configure some beans for an IDP provider.

@SpringBootTest(classes = WebITApplication.class,
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(SpringExtension.class)
@ActiveProfiles("web-it")
class WebJwtIt {

    @LocalServerPort
    private int port;

    @Autowired
    private WebClient webClient;

    @Test
    void testIdpJwt() {

        String response = webClient.get().uri("http://localhost:" + port + "/web/it")
                .attributes(ServletOAuth2AuthorizedClientExchangeFilterFunction.clientRegistrationId("ping")).retrieve()
                .bodyToMono(String.class).block();
        assertThat(response).isEqualTo("pass");
    }

    @RestController
    @SpringBootApplication
    @ImportAutoConfiguration(IdpAutoConfiguration.class)
    static class WebITApplication implements IdpSecurityAdapter {

              @Bean
    WebClient webClient(ClientRegistrationRepository clientRegistrations,
            OAuth2AuthorizedClientRepository authorizedClients) {
        ServletOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServletOAuth2AuthorizedClientExchangeFilterFunction(
                clientRegistrations, authorizedClients);
        // (optional) explicitly opt into using the oauth2Login to provide an access token implicitly
        // oauth.setDefaultOAuth2AuthorizedClient(true);
        // (optional) set a default ClientRegistration.registrationId
        // oauth.setDefaultClientRegistrationId("client-registration-id");
        return WebClient.builder().apply(oauth.oauth2Configuration()).build();
    }
        public static void main(String args[]) {

            new SpringApplicationBuilder().profiles("web-it").build().run(WebITApplication.class, args);
        }

        @GetMapping
        public String secured() {
            return "secured";
        }

        @GetMapping("/web/it")
        public String securedOne() {
            return "pass";
        }

        @Override
        public void configure(final HttpSecurity httpSecurity) throws IdpSecurityAdapterException {
            try {
                httpSecurity.oauth2Client();
            } catch (Exception e) {
                throw new IdpSecurityAdapterException("Failed to Configure Oauth2Client", e);
            }
        }

        @Override
        public IdpProvider getIdpProvider() {
            return IdpProvider.MyIdp;
        }
    }

Is there anyway for a webclient to get the token for me and add it to the request? I know this was possible with the spring-security-oauth:OAuthRestTemplate and reading the documentation I thought it was possible with a web client.

Florent Gornes :

The problem here is that you are not instancing your WebClient the right way.

As you are on the client side, you do not have access to an OAuth2AuthorizedClientRepository. This bean is supposed to be linked to a resource sever on which you log into using the .oauth2Login() method declaration on the HttpSecurityconfiguration. These details are explained here : Spring Security 5 Oauth2 Login.

Again, you are on the client side so what you need is an exchange filter function which will trigger a request to an authorization server to get a JWT token. You can use the ServerOAuth2AuthorizedClientExchangeFilterFunction instead.

You better use a WebClientCustomizer to add the exchange filter function in the WebClient filter. Why ? Simply because injecting in your Spring application a WebClient.Builder will allow you accessing native metrics linked to web exchanges.

Hence, you will build your WebClient using a new instance of an UnAuthenticatedServerOAuth2AuthorizedClientRepository bean like this :

// Use injection to get an in-memory reposiroty or client registrations
@Bean
WebClient webClient(ClientRegistrationRepository clientRegistrations) {

    // Provides support for an unauthenticated user such as an application
    ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(
            clientRegistrations, new UnAuthenticatedServerOAuth2AuthorizedClientRepository());

    // Build up a new WebClientCustomizer implementation to inject the oauth filter
    // function into the WebClient.Builder instance
    return new WebClientSecurityCustomizer(oauth);
}

As you are on the client side, you are not associating a user to your process, that's why you can not use any authorized client repository bean instanciation. Check on Spring Security documentation : Spring Security documentation : Class UnAuthenticatedServerOAuth2AuthorizedClientRepository.

I tried to sum up a demo case in the following GitHub project : GitHub - Spring Security OAuth2 Machine-To-Machine scenario.

I hope this gives you more insights on WebClient configuration. Please ask if you have any question.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=90617&siteId=1