DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Distributed Tracing System (Spring Cloud Sleuth + OpenZipkin)
  • How To Build Web Service Using Spring Boot 2.x
  • Authentication With Remote LDAP Server in Spring Web MVC
  • Spring Security Oauth2: Google Login

Trending

  • DZone's Article Submission Guidelines
  • The End of “Good Enough Agile”
  • Event Driven Architecture (EDA) - Optimizer or Complicator
  • Rust, WASM, and Edge: Next-Level Performance
  1. DZone
  2. Coding
  3. Frameworks
  4. Easy Access to OAuth 2.0 Protected Resources With the Spring WebClient

Easy Access to OAuth 2.0 Protected Resources With the Spring WebClient

Learn how to secure an application with Okta OIDC login, access a third-party OAuth 2 resource with Spring WebClient, and more.

By 
Jimena Garbarino user avatar
Jimena Garbarino
·
Oct. 18, 21 · Tutorial
Likes (3)
Comment
Save
Tweet
Share
4.4K Views

Join the DZone community and get the full member experience.

Join For Free

Spring Framework 5.0 introduced Spring WebClient as part of the WebFlux reactive web stack. WebClient is a reactive HTTP client that provides a functional and fluent API based on Reactor, allowing declarative composition of asynchronous non-blocking requests.  No need to  manage concurrency issues. Support for filter registration means it can 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 application testing. With a testing interface wrapper to check replies, WebClient can be used to execute end-to-end HTTP testing and connect to a live server. It can also bind to a controller or application context and simulate requests and responses without requiring a running server.

Spring Security 5 includes WebTestClient integration and SecurityMockServerConfigurers, which can be used to mock authenticated users and authorized clients during testing to prevent authorization handshakes. This feature is useful for secured applications that access third-party OAuth 2.0 resources.  We'll explore this in-depth 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

  • HTTPie
  • Java 11+
  • Okta CLI

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:

Java
 
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:

Java
 
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?

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

Java
 
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:

Java
 
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:

Java
 
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:

Java
 
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:

Java
 
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.

Edit your application.yml and set the following properties:

Java
 
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:

Java
 
<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:

Java
 
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:

Java
 
./mvnw test


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

Java
 
./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:

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:

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

After you authorize access, the search result will show:

Java
 
{"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.

  • Spring Security — WebClient OAuth2 Setup
  • Spring Framework — WebClient Testing
  • Spring Security — @RegisteredOAuth2AuthorizedClient

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:

  • How to Use Client Credentials Flow with Spring Security
  • Reactive Java Microservices with Spring Boot and JHipster
  • Secure Reactive Microservices with Spring Cloud Gateway

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

Spring Framework Spring Security authentication application GitHub Requests Java (programming language) Spring Boot integration test Flow (web browser) Web Protocols

Published at DZone with permission of Jimena Garbarino, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Distributed Tracing System (Spring Cloud Sleuth + OpenZipkin)
  • How To Build Web Service Using Spring Boot 2.x
  • Authentication With Remote LDAP Server in Spring Web MVC
  • Spring Security Oauth2: Google Login

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!