Spring WebClient for Easy Access to OAuth 2.0 Protected Resources

Spring ẀebClient was added as part of the reactive web stack WebFlux in Spring Framework 5.0. WebClient allows performing HTTP requests in reactive applications, providing a functional and fluent API based on Reactor, and enabling a declarative composition of asynchronous non-blocking requests without the need to deal with concurrency. One of its features is support for filter registration, allowing to intercept and modify requests, which can be used for cross-cutting concerns such as authentication, as demonstrated in this tutorial.

The WebTestClient is also an HTTP client designed for testing applications. It wraps WebClient and provides a testing facade for verifying responses. It can be used to perform end-to-end HTTP tests, binding to a running server. It can also bind to a controller or application context and mock requests and responses without the need for a running server.

Spring Security 5 provides integration with WebTestClient, and ships with SecurityMockServerConfigurers that can be used to mock authenticated users and authorized clients to avoid authorization handshakes during testing. This feature is handy for secured applications that access third-party OAuth 2.0 resources, as it will be explored in the following sections.

In this tutorial, you will learn how to:

  • Secure an application with Okta OIDC Login
  • Access a third-party OAuth 2 resource with Spring WebClient
  • Carry out integration testing for code that uses WebClient
  • Use mock third-party authorization in WebTestClient

Prerequisites:

Table of Contents

Create a Secure Microservice with Okta Authentication

Start by building a simple microservice that returns the total count of a GitHub code search by keyword. The third-party service in this example is GitHub REST API.

Create a microservice application using Spring Initializr and HTTPie. In a terminal window, request a Spring Boot Maven project with webflux for reactive, okta for security and lombok:

http -d https://start.spring.io/starter.zip type==maven-project \
  language==java \
  bootVersion==2.4.5 \
  baseDir==search-service \
  groupId==com.okta.developer \
  artifactId==search-service \
  name==search-service \
  packageName==com.okta.developer.search \
  javaVersion==11 \
  dependencies==webflux,okta,lombok

NOTE: The Lombok dependency allows reducing boilerplate code with annotations, but requires additional configuration in your IDE to work. Check out the instructions at projectlombok.org

Unzip the project:

unzip search-service.zip
cd search-service

Before you begin, you’ll need a free Okta developer account. Install the Okta CLI and run okta register to sign up for a new account. If you already have an account, run okta login. Then, run okta apps create. Select the default app name, or change it as you see fit. Choose Web and press Enter.

Select Okta Spring Boot Starter. Accept the default Redirect URI values provided for you. That is, a Login Redirect of http://localhost:8080/login/oauth2/code/okta and a Logout Redirect of http://localhost:8080.

What does the Okta CLI do?

The Okta CLI will create an OIDC Web App in your Okta Org. It will add the redirect URIs you specified and grant access to the Everyone group. You will see output like the following when it’s finished:

Okta application configuration has been written to: 
  /path/to/app/src/main/resources/application.properties

Open src/main/resources/application.properties to see the issuer and credentials for your app.

okta.oauth2.issuer=https://dev-133337.okta.com/oauth2/default
okta.oauth2.client-id=0oab8eb55Kb9jdMIr5d6
okta.oauth2.client-secret=NEVER-SHOW-SECRETS

NOTE: You can also use the Okta Admin Console to create your app. See Create a Spring Boot App for more information.

Rename application.properties to application.yml, the following properties must be set:

okta:
  oauth2:
    issuer: https://{yourOktaDomain}/oauth2/default
    client-secret: {clientSecret}
    client-id: {clientId}
    scopes: openid, profile, email

service:
  github: https://api.github.com

Add security configuration for enabling OIDC authentication in the application. Create the package com.okta.developer.search.security and add the class SecurityConfiguration:

package com.okta.developer.search.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.RedirectServerAuthenticationEntryPoint;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
public class SecurityConfiguration {

    @Bean
    public SecurityWebFilterChain configure(ServerHttpSecurity http) {

        http
            .authorizeExchange((exchanges) ->
                exchanges.anyExchange().authenticated()
            )
            .oauth2Login(withDefaults())
            .exceptionHandling()
            .authenticationEntryPoint(new RedirectServerAuthenticationEntryPoint("/oauth2/authorization/okta"));
        return http.build();

    }
}

In the configuration above, any request must be authenticated, and the authentication entry point will redirect to Okta. The entry point definition is required as a second client registration will be configured later in the tutorial for the third-party requests. For this example, login is enabled, to verify the flow with the browser.

Create a com.okta.developer.search.controller package and add a SearchCount class for the totalCount result:

package com.okta.developer.search.controller;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SearchCount {

    @JsonProperty("total_count")
    private Integer totalCount;
}

Then add a SearchController class:

package com.okta.developer.search.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import static org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient;

@RestController
public class SearchController {

    @Value("${service.github}")
    private String apiUrl;

    @Autowired
    private WebClient webClient;


    @GetMapping(value = "/totalCount/{keyword}")
    public Mono<SearchCount> getSearchCountAuthorized(@PathVariable String keyword,
                                                      @RegisteredOAuth2AuthorizedClient( "github")OAuth2AuthorizedClient authorizedClient){
        return this.webClient.get().uri(apiUrl, uriBuilder -> uriBuilder
                .path("/search/code")
                .queryParam("q", keyword).build())
                .attributes(oauth2AuthorizedClient(authorizedClient))
                .retrieve()
                .bodyToMono(SearchCount.class);
    }

}

Independent of how the user authenticates, in this case using Okta, another client registration is in play for the search request.

The SearchController requires a github authorized client, that is set as an attribute in the WebClient. With this parameter, Spring Security will resolve the access token for accessing the GitHub REST API. Notice the controller automatically wires a WebClient instance that will be configured in the next section, along with the GitHub client registration. Also, notice the mentioned declarative composition of the request. The URI, parameters, request attributes, and response extraction are defined through method chaining.

Access an OAuth 2.0 Third-Party Resource with Spring WebClient

For the WebClient to handle the GitHub grant flow, it must include the ServerOAuth2AuthorizedClientExchangeFilterFunction filter. Create the package com.okta.developer.search.configuration and add the class WebClientConfiguration:

package com.okta.developer.search.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.security.oauth2.client.web.server.ServerOAuth2AuthorizedClientRepository;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class WebClientConfiguration {

    @Bean
    public WebClient webClient(ReactiveClientRegistrationRepository clientRegistrations,
                               ServerOAuth2AuthorizedClientRepository authorizedClients)  {
        ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
                new ServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, authorizedClients);

        return WebClient.builder()
                .filter(oauth)
                .build();
    }
}

The ServerOAuth2AuthorizedClientExchangeFilterFunction provides a mechanism for using an OAuth2AuthorizedClient to make requests including a Bearer Token, and supports the following features:

  • setDefaultOAuth2AuthorizedClient: explicitly opt into using oauth2Login() to provide an access token implicitly. For this use case, the WebClient is used to access a third-party service, so the tokens obtained from Okta are not valid for accessing GitHub.
  • setDefaultClientRegistrationId: set a default ClientRegistration.registrationId. This could be a useful option, as the WebClient would be already initialized with the required GitHub client registration. But for some reason, the integration test will not pick up the mock client registration, and a grant flow will be triggered to production GitHub. That’s the reason why instead of using this feature, the WebClient is passed the GitHub authorized client through attributes when invoked in the SearchController.

For the GitHub client registration, you need a GitHub account. Sign in and go to the top-right user menu and choose Settings. Then on the left menu, choose Developer settings. From the left menu, select OAuth Apps, then click on New OAuth App. For the example, set the following values:

  • Application name: search-service
  • Homepage URL: http://localhost:8080
  • Authorization callback URL: http://localhost:8080

Click Register application. Now, on the application page, click on Generate a new client secret. Copy the Client ID and the generated Client secret.

GitHub OAuth Application clientID and secret

Edit your application.yml and set the following properties:

spring:
  security:
    oauth2:
      client:
        registration:
          github:
            client-id: {githubClientID}
            client-secret: {githubClientSecret}

Spring WebClient Testing with MockWebServer

For testing code that uses the WebClient, Spring Framework documentation recommends OkHttp MockWebServer, in the WebClient chapter. This recommendation should also be included in the Integration Testing chapter, as only clients that use RestTemplate internally are mentioned in that chapter. Different aspects of Spring WebClient and WebTestClient are covered across the three references Spring Framework, Spring Boot, and Spring Security, and navigating through documentation is not an easy task.

For this example, the MockWebServer will mock the GitHub REST API. With this library and the help of Spring Security Test, hitting to production can be avoided, and the third-party authorization can be mocked.

Edit your pom.xml to add Spring Security Test and MockWebServer dependencies:

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <version>5.4.5</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.0.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>mockwebserver</artifactId>
    <version>4.0.1</version>
    <scope>test</scope>
</dependency>

Create the test class SearchControllerIntegrationTest with the following code:

package com.okta.developer.search;

import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.reactive.server.WebTestClient;

import java.io.IOException;

import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOAuth2Client;
import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockOidcLogin;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
public class SearchControllerIntegrationTest {

    @Autowired
    private WebTestClient webTestClient;

    public static MockWebServer mockWebServer;

    @BeforeAll
    static void setUp() throws IOException {
        mockWebServer = new MockWebServer();
        mockWebServer.start();
    }

    @DynamicPropertySource
    static void properties(DynamicPropertyRegistry registry) throws IOException {
        registry.add("service.github", () -> "http://localhost:" + mockWebServer.getPort());
    }

    @Test
    public void testGetTotalCount(){

        mockWebServer.enqueue(new MockResponse()
                .addHeader("Content-Type", "application/json; charset=utf-8")
                .setBody("{ \"totalCount\": 9}"));

        webTestClient
                .mutateWith(mockOidcLogin())
                .mutateWith(mockOAuth2Client("github"))
                .get()
                .uri("/totalCount/bleble")
                .accept(MediaType.APPLICATION_JSON)
                .exchange()
                .expectStatus().isOk()
                .expectBody().jsonPath("$.tocalCount", 9);
    }

    @AfterAll
    static void tearDown() throws IOException {
        mockWebServer.shutdown();
    }
}

Let’s break down what is going on in this test.

The @BeforeAll and @AfterAll annotated methods start and shut down the MockWebServer that will mock GitHub REST API. The @DynamicPropertySource annotated method overwrites the GitHub REST API URL and makes it point to the mockWebServer instance.

The call to mockWebServer.enqueue() sets up the mock response for the third-party call.

In the WebTestClient, the test request is configured with a mock OidcUser with the mutateWith(mockOidcLogin()) call, avoiding some kind of simulation of the grant flow with the Okta authorization server.

As mentioned before, independent of how the user authenticates, there is a second client registration in play for the request under test. The call mutateWith(mockOAuth2Client("github")) creates an OAuth2AuthorizedClient, avoiding the simulation of a handshake with the third-party authorization server.

Run the integration test with Maven:

./mvnw test

Now, run the application to manually verify the request flow with a browser.

./mvnw spring-boot:run

Open an incognito window and go to http://localhost:8080/totalCount/root:%20DEBUG to request a code search for the string root: DEBUG.

First, the authentication flow will redirect to Okta for the login:

Okta Login Form

Once you sign in with your Okta user, Spring Security will trigger the GitHub authorization flow, to instantiate the authorized client required for the API request:

GitHub Login Form

After the GitHub login, then you must authorize the search application for sending requests to the GitHub REST API on your behalf:

GitHub Authorization Page

After you authorize access, the search result will show:

{"total_count":114761362}

Learn More About Reactive Spring Boot and WebClient

I hope you enjoyed this tutorial and got a clear picture of WebClient testing for applications that access third-party services. As mentioned before, WebClient and WebTestClient documentation pieces are covered in the three references, Spring Framework, Spring Boot, and Spring Security, so I recommend taking a thorough look at all of them.

An additional aspect to explore is how to add layering, given that client registrations must be injected as controller parameters, to trigger the authorization flows when required. Keep learning about Reactive Applications with Spring Boot, and OAuth security, and check out the following links:

You can find the application code on GitHub in the okta-spring-webclient-example repository.

If you have any questions about this post, please ask in the comments below. For more Spring content, follow @oktadev on Twitter, like us on Facebook, or subscribe to our YouTube channel.