How to Use Spring Cloud Gateway With OAuth 2.0 Patterns
The reactive AI Gateway of the Spring Ecosystem—built on Spring Boot, WebFluz, and Project—is Spring Cloud Gateway. Spring Cloud Gateway’s job is to factor and route requests to services, as well as provide alternative concerns, like security, monitoring, and resilience. While Reactive models gain popularity, the microservices you use will most likely be a combination of Spring MVC blocking applications and Spring WebFlux non-blocking applications.
This article will show you how to use Spring Cloud Gateway for routing as well as traditional Servlet API microservices. You will also learn the necessary configurations for OpenID Configuration Authentication, Token Relay, and Client Credentials Grant, all of which are common OAuth2 patterns that use Okta as an authorization server.
Prerequisites:
Java 11+
cURL
Table of Contents
- Pattern 1: OpenID Connect Authentication
- Create a Eureka Discovery Service
- Create a Spring Cloud Gateway Application
- Pattern 2: Token Relay to Service
- Create a REST API Service
- Route the REST API Through Spring Cloud Gateway
- Pattern 3: Service-to-Service Client Credentials Grant
- Create a Microervice
- Secure the Micro Service using OAuth 2.0 Scopes
- Update the REST API to Call the Micro Service
- Putting it All Together
- Learn More About Building Secure Applications
Pattern 1: OpenID Connect Authentication
OpenID Connect defines a mechanism for end-user authentication based on the OAuth2 authorization code flow. In this pattern, the Authorization Server returns an Authorization Code to the application, which can then exchange it for an ID Token and an Access Token directly. The Authorization Server authenticates the application with a ClientId and ClientSecret before the exchange happens. As you can see in the diagram below, OpenID and OAuth2 patterns make extensive use of HTTP redirections, some of which have been omitted for clarity.
For testing this OAuth2 pattern, create the API Gateway with service discovery. First, create a base folder for all the projects:
xxxxxxxxxx
mkdir oauth2-patterns
cd oauth2-patterns
Create a Eureka Discovery Service
With Spring Initializr, create an Eureka server:
xxxxxxxxxx
curl https://start.spring.io/starter.zip -d dependencies=cloud-eureka-server \
-d groupId=com.okta.developer \
-d artifactId=discovery-service \
-d name="Eureka Service" \
-d description="Discovery service" \
-d packageName=com.okta.developer.discovery \
-d javaVersion=11 \
-o eureka.zip
Unzip the file:
xxxxxxxxxx
unzip eureka.zip -d eureka
cd eureka
As described in Spring Cloud Netflix documentation, the JAXB modules, which the Eureka server depends upon, were removed in JDK 11. If you are running Eureka server with JDK 11, edit the pom.xml
and add the following dependency:
xxxxxxxxxx
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
</dependency>
Edit EurekaServiceApplication
to add @EnableEurekaServer
annotation:
xxxxxxxxxx
package com.okta.developer.discovery;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
public class EurekaServiceApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServiceApplication.class, args);
}
}
Rename src/main/resources/application.properties
to application.yml
and add the following content:
xxxxxxxxxx
server
port8761
eureka
instance
hostname localhost
client
registerWithEurekafalse
fetchRegistryfalse
serviceUrl
defaultZone http //$ eureka.instance.hostname $ server.port /eureka/
Start the service:
xxxxxxxxxx
./mvnw spring-boot:run
Go to http://localhost:8761
and you should see the Eureka home.
Create a Spring Cloud Gateway Application
Now let’s create an API Gateway with Spring Cloud Gateway, using Spring Initializr again.
xxxxxxxxxx
curl https://start.spring.io/starter.zip \
-d dependencies=cloud-eureka,cloud-gateway,webflux,okta,cloud-security,thymeleaf \
-d groupId=com.okta.developer \
-d artifactId=api-gateway \
-d name="Spring Cloud Gateway Application" \
-d description="Demo project of a Spring Cloud Gateway application and OAuth flows" \
-d packageName=com.okta.developer.gateway \
-d javaVersion=11 \
-o api-gateway.zip
Unzip the project:
xxxxxxxxxx
unzip api-gateway.zip -d api-gateway
cd api-gateway
Edit SpringCloudGatewayApplication
to add @EnableEurekaClient
annotation.
xxxxxxxxxx
package com.okta.developer.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
public class SpringCloudGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(SpringCloudGatewayApplication.class, args);
}
}
Add a package controller
and the controller class src/main/java/com/okta/developer/gateway/controller/GreetingController.java
. The controller will allow us to test the login without having configured any routes yet.
xxxxxxxxxx
package com.okta.developer.gateway.controller;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
public class GreetingController {
"/greeting") (
public String greeting( OidcUser oidcUser, Model model,
"okta") OAuth2AuthorizedClient client) { (
model.addAttribute("username", oidcUser.getEmail());
model.addAttribute("idToken", oidcUser.getIdToken());
model.addAttribute("accessToken", client.getAccessToken());
return "greeting";
}
}
Add a greeting template src/main/resources/templates/greeting.html
:
xxxxxxxxxx
<html xmlns:th="https://www.thymeleaf.org">
<head>
<title>Greeting</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h1><p th:text="'Hello, ' + ${username} + '!'" ></p></h1>
<p th:text="'idToken: ' + ${idToken.tokenValue}" ></p><br/>
<p th:text="'accessToken: ' + ${accessToken.tokenValue}" ></p><br>
</body>
</html>
Rename src/main/resources/application.properties
to application.yml
and add the following properties:
xxxxxxxxxx
spring
application
name gateway
cloud
gateway
discovery
locator
enabledtrue
logging
level
org.springframework.cloud.gateway DEBUG
reactor.netty DEBUG
Add src/main/java/com/okta/developer/gateway/OktaOAuth2WebSecurity.java
to make the API Gateway a resource server with login enabled:
xxxxxxxxxx
package com.okta.developer.gateway;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
public class OktaOAuth2WebSecurity {
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http.csrf().disable()
.authorizeExchange()
.anyExchange()
.authenticated()
.and().oauth2Login()
.and().oauth2ResourceServer().jwt();
return http.build();
}
}
To keep things simple in this example, CSRF is disabled.
Now we need to create an authorization client in Okta. Log in to your Okta Developer account (or sign up if you don’t have an account).
- From the Applications page, choose Add Application.
- On the Create New Application page, select Web.
- Name your app Api Gateway, add
http://localhost:8080/login/oauth2/code/okta
as a Login redirect URI, select Refresh Token (in addition to Authorization Code), and click Done.
Copy the issuer (found under API > Authorization Servers), client ID, and client secret.
Start the gateway with:
xxxxxxxxxx
OKTA_OAUTH2_ISSUER={yourOktaIssuer} \
OKTA_OAUTH2_CLIENT_ID={yourOktaClientId} \
OKTA_OAUTH2_CLIENT_SECRET={yourOktaClientSecret} \
./mvnw spring-boot:run
Go to http://localhost:8080/greeting
. The gateway will redirect to Okta login page:
After the login, the idToken and accessToken will be displayed in the browser.
xxxxxxxxxx
idToken eyJraWQiOiIw...
accessToken eyJraWQiOi...
Pattern 2: Token Relay to Service
A Token Relay happens when an OAuth2 consumer, for example the API Gateway, acts as a Client and forwards the accessToken to the routed service.
Create a REST API Service
Let’s create a shopping cart service.
xxxxxxxxxx
curl https://start.spring.io/starter.zip -d dependencies=web,data-jpa,h2,cloud-eureka,okta,security,lombok \
-d groupId=com.okta.developer \
-d artifactId=cart-service \
-d name="Cart Service" \
-d description="Demo cart microservice" \
-d packageName=com.okta.developer.cartservice \
-d javaVersion=11 \
-o cart-service.zip
Unzip the file:
xxxxxxxxxx
unzip cart-service.zip -d cart-service
cd cart-service
Edit pom.xml
and add Jackson Datatype Money dependency. For this tutorial, we will use JavaMoney for currency.
xxxxxxxxxx
<dependency>
<groupId>org.zalando</groupId>
<artifactId>jackson-datatype-money</artifactId>
<version>1.1.1</version>
</dependency>
Before creating the entities for the service, add a MonetaryAmountConverter
for mapping the MonetaryAmount
type to the database. The conversion to and from BigDecimal for persistence allows using the built-in Hibernate BigDecimal to JDBC numeric mapping. Add src/main/java/com/okta/developer/cartservice/model/
MonetaryAmountConverter.java
:
xxxxxxxxxx
package com.okta.developer.cartservice.model;
import javax.money.CurrencyUnit;
import javax.money.Monetary;
import javax.money.MonetaryAmount;
import javax.persistence.AttributeConverter;
import java.math.BigDecimal;
public class MonetaryAmountConverter implements AttributeConverter<MonetaryAmount, BigDecimal> {
private final CurrencyUnit USD = Monetary.getCurrency("USD");
public BigDecimal convertToDatabaseColumn(MonetaryAmount monetaryAmount) {
if (monetaryAmount == null){
return null;
}
return monetaryAmount.getNumber().numberValue(BigDecimal.class);
}
public MonetaryAmount convertToEntityAttribute(BigDecimal bigDecimal) {
if (bigDecimal == null){
return null;
}
return Monetary.getDefaultAmountFactory()
.setCurrency(USD)
.setNumber(bigDecimal.doubleValue())
.create();
}
}
Create the Cart
and LineItem
model classes under src/main/java/com/okta/developer/cartservice/model
package:
xxxxxxxxxx
package com.okta.developer.cartservice.model;
import lombok.Data;
import javax.money.MonetaryAmount;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
public class Cart {
strategy= GenerationType.AUTO) (
private Integer id;
private String customerId;
converter=MonetaryAmountConverter.class) (
private MonetaryAmount total;
cascade = CascadeType.ALL) (
private List<LineItem> lineItems = new ArrayList<>();
public void addLineItem(LineItem lineItem) {
this.lineItems.add(lineItem);
}
}
xxxxxxxxxx
package com.okta.developer.cartservice.model;
import lombok.Data;
import javax.money.MonetaryAmount;
import javax.persistence.*;
public class LineItem {
strategy= GenerationType.AUTO) (
private Integer id;
private String productName;
private Integer quantity;
converter=MonetaryAmountConverter.class) (
private MonetaryAmount price;
}
Add a CartRepository
under src/main/java/com/okta/developer/cartservice/repository/CartRepository.java
:
xxxxxxxxxx
package com.okta.developer.cartservice.repository;
import com.okta.developer.cartservice.model.Cart;
import org.springframework.data.repository.CrudRepository;
public interface CartRepository extends CrudRepository<Cart, Integer> {
}
Create the CartNotFoundException
under src/main/java/com/okta/developer/cartservice/controller/CartNotFoundException.java
for mapping the API 404 in the CartController
we will create after:
xxxxxxxxxx
package com.okta.developer.cartservice.controller;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
HttpStatus.NOT_FOUND) (
public class CartNotFoundException extends RuntimeException {
public CartNotFoundException(String message) {
super(message);
}
}
Add a CartController
under src/main/java/com/okta/developer/cartservice/controller/CartController.java
:
xxxxxxxxxx
package com.okta.developer.cartservice.controller;
import com.okta.developer.cartservice.model.Cart;
import com.okta.developer.cartservice.repository.CartRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
public class CartController {
private CartRepository repository;
"/cart/{id}") (
public Cart getCart( Integer id){
return repository.findById(id).orElseThrow(() -> new CartNotFoundException("Cart not found:" + id));
}
"/cart") (
public Cart saveCart( Cart cart){
Cart saved = repository.save(cart);
return saved;
}
}
Configure the Jackson Money Datatype module. Add a WebConfig
class under src/main/java/com/okta/developer/cartservice
:
xxxxxxxxxx
package com.okta.developer.cartservice;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.zalando.jackson.datatype.money.MoneyModule;
public class WebConfig implements WebMvcConfigurer {
public MoneyModule moneyModule(){
return new MoneyModule().withDefaultFormatting();
}
}
Edit CartServiceApplication
and add @EnableEurekaClient
:
xxxxxxxxxx
package com.okta.developer.cartservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
public class CartServiceApplication {
public static void main(String[] args) {
SpringApplication.run(CartServiceApplication.class, args);
}
}
Rename src/main/resources/application.propeties
to application.yml
and add the following values:
xxxxxxxxxx
server
port8081
spring
application
name cart
okta
oauth2
issuer yourOktaIssuer
audience api //default
Start the cart-service
:
xxxxxxxxxx
./mvnw spring-boot:run
Route the REST API Through Spring Cloud Gateway
Go to the api-gateway
project and add a route for the cart service, edit SpringCloudGatewayApplication
:
xxxxxxxxxx
package com.okta.developer.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.security.oauth2.gateway.TokenRelayGatewayFilterFactory;
import org.springframework.context.annotation.Bean;
public class SpringCloudGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(SpringCloudGatewayApplication.class, args);
}
public RouteLocator routeLocator(RouteLocatorBuilder builder, TokenRelayGatewayFilterFactory filterFactory) {
return builder.routes()
.route("cart", r -> r.path("/cart/**")
.filters(f -> f.filter(filterFactory.apply()))
.uri("lb://cart"))
.build();
}
}
TokenRelayGatewayFilterFactory
will find the accessToken from the registered OAuth2 client and include it in the outbound cart request.
Restart the gateway with:
xxxxxxxxxx
OKTA_OAUTH2_ISSUER={yourOktaIssuer} \
OKTA_OAUTH2_CLIENT_ID={clientId} \
OKTA_OAUTH2_CLIENT_SECRET={clientSecret} \
./mvnw spring-boot:run
Go to http://localhost:8080/greeting
and copy the accessToken. Then use the accessToken to make requests to the cart API through the gateway.
xxxxxxxxxx
export ACCESS_TOKEN={accessToken}
# Add an item to the cart
curl \
-d '{"customerId": "uijoon@example.com", "lineItems": [{ "productName": "jeans", "quantity": 1}]}' \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
http://localhost:8080/cart
# Return the contents of the cart
curl \
-H 'Accept: application/json' \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
http://localhost:8080/cart/1
Pattern 3: Service-to-Service Client Credentials Grant
In this authorization pattern, the application requests an access token using only its own client credentials. This flow is suitable for machine-to-machine (M2M) or service-to-service authorizations.
Create a Microservice
For service-to-service authorization, create a pricing
Spring Boot service with Spring Initializr:
xxxxxxxxxx
curl https://start.spring.io/starter.zip -d dependencies=web,cloud-eureka,okta,security,lombok \
-d groupId=com.okta.developer \
-d artifactId=pricing-service \
-d name="Pricing Service" \
-d description="Demo pricing microservice" \
-d packageName=com.okta.developer.pricing \
-d javaVersion=11 \
-o pricing-service.zip
Unzip the file:
xxxxxxxxxx
unzip pricing-service.zip -d pricing-service
cd pricing-service
Edit pom.xml
and add Jackson Datatype Money dependency again.
xxxxxxxxxx
<dependency>
<groupId>org.zalando</groupId>
<artifactId>jackson-datatype-money</artifactId>
<version>1.1.1</version>
</dependency>
Create the src/main/java/com/okta/developer/pricing/model/Cart.java
and src/main/java/com/okta/developer/pricing/model/LineItem.java
model classes:
xxxxxxxxxx
package com.okta.developer.pricing.model;
import lombok.Data;
import javax.money.MonetaryAmount;
import java.util.ArrayList;
import java.util.List;
public class Cart {
private Integer id;
private String customerId;
private List<LineItem> lineItems = new ArrayList<>();
private MonetaryAmount total;
public void addLineItem(LineItem lineItem){
this.lineItems.add(lineItem);
}
}
xxxxxxxxxx
package com.okta.developer.pricing.model;
import lombok.Data;
import javax.money.MonetaryAmount;
public class LineItem {
private Integer id;
private Integer quantity;
private MonetaryAmount price;
private String productName;
public LineItem(){
}
public LineItem(Integer id, Integer quantity) {
this.id = id;
this.quantity = quantity;
}
}
Create the src/main/java/com/okta/developer/pricing/service/PricingService.java
interface and a src/main/java/com/okta/developer/pricing/service/DefaultPricingService
implementation to calculate prices for the LineItem
.
xxxxxxxxxx
package com.okta.developer.pricing.service;
import com.okta.developer.pricing.model.Cart;
public interface PricingService {
Cart price(Cart cart);
}
xxxxxxxxxx
package com.okta.developer.pricing.service;
import com.okta.developer.pricing.model.Cart;
import com.okta.developer.pricing.model.LineItem;
import org.springframework.stereotype.Service;
import javax.money.CurrencyUnit;
import javax.money.Monetary;
import javax.money.MonetaryAmount;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class DefaultPricingService implements PricingService {
private final CurrencyUnit USD = Monetary.getCurrency("USD");
public Cart price(Cart cart) {
MonetaryAmount total = Monetary.getDefaultAmountFactory()
.setCurrency(USD)
.setNumber(0)
.create();
for (LineItem li : cart.getLineItems()) {
BigDecimal bigDecimal = new BigDecimal(Math.random() * 100)
.setScale(2, RoundingMode.HALF_UP);
MonetaryAmount amount = Monetary.getDefaultAmountFactory()
.setCurrency(USD)
.setNumber(bigDecimal)
.create();
li.setPrice(amount);
total = total.add(amount.multiply(li.getQuantity()));
}
cart.setTotal(total);
return cart;
}
}
Create a PricingController
to handle the pricing request. Add the class src/main/java/com/okta/developer/pricing/controller/PricingController.java
:
xxxxxxxxxx
package com.okta.developer.pricing.controller;
import com.okta.developer.pricing.model.Cart;
import com.okta.developer.pricing.service.PricingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
public class PricingController {
private PricingService pricingService;
"/pricing/price") (
public Cart price( Cart cart){
Cart priced = pricingService.price(cart);
return priced;
}
}
Configure the Jackson Money Module in a src/main/java/com/okta/developer/pricing/WebConfig.java
class:
xxxxxxxxxx
package com.okta.developer.pricing;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.zalando.jackson.datatype.money.MoneyModule;
public class WebConfig implements WebMvcConfigurer {
public MoneyModule moneyModule(){
return new MoneyModule().withDefaultFormatting();
}
}
Secure the Micro Service Using OAuth 2.0 Scopes
Protect the pricing endpoint by requiring a custom scope pricing
in the accessToken. One way to do it is with HttpSecurity
configuration. Add a src/main/java/com/okta/developer/pricing/WebSecurity.java
class with the following:
xxxxxxxxxx
package com.okta.developer.pricing;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
public class WebSecurity extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests -> authorizeRequests
.mvcMatchers("/pricing/**").hasAuthority("SCOPE_pricing")
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
}
}
Add @EnableEurekaClient
to PricingServiceApplication
:
xxxxxxxxxx
package com.okta.developer.pricing;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
public class PricingServiceApplication {
public static void main(String[] args) {
SpringApplication.run(PricingServiceApplication.class, args);
}
}
Rename src/main/resources/application.properties
to application.yml
and add the following:
xxxxxxxxxx
server
port8082
spring
application
name pricing
okta
oauth2
issuer yourOktaIssuer
audience api //default
Start the service:
xxxxxxxxxx
./mvnw spring-boot:run
Let’s try the pricing API without an accessToken:
xxxxxxxxxx
curl -v\
-d '{"customerId": "uijoon@mail.com", "lineItems": [{ "productName": "jeans", "quantity": 1}]}' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
http://localhost:8082/pricing/price
With the -v
verbose flag, you should see the request is rejected with 401 (Unauthorized).
Update the REST API to Call the Microservice
Now we are going to configure cart-service
to use the client credentials grant flow to request pricing.
First create a new authorization client in Okta.
- From the Applications page, choose Add Application
- On the Create New Application page, select Service
- Name your app Cart Service and click Done
Copy the new client ID, and client secret.
Create a custom scope to restrict what the cart-service
accessToken can access. From the menu bar select API -> Authorization Servers. Edit the authorization server by clicking on the edit pencil, then click Scopes -> Add Scope. Fill out the name field with pricing
and press Create.
We need to configure the OAuth2 client in the cart-service
application, for calling the pricing-service
. OAuth2RestTemplate
is not available in Spring Security 5.3.x. According to Spring Security OAuth migration guides, the way to do this is by using RestTemplate interceptors or WebClient exchange filter functions. Since Spring 5, RestTemplate
is in maintenance mode, using WebClient (which supports sync, async, and streaming scenarios) is the suggested approach. So let’s configure a WebClient
for the pricing call.
First, add the spring-webflux
starter dependency to the cart-service
pom.xml
:
xxxxxxxxxx
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
IMPORTANT: Adding both spring-boot-starter-web
and spring-boot-starter-webflux
modules results in Spring Boot auto-configuring Spring MVC, not WebFlux. This allows Spring MVC applications to use the reactive WebClient.
Create src/main/java/com/okta/developer/cartservice/WebClientConfig.java
:
xxxxxxxxxx
package com.okta.developer.cartservice;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.codec.json.Jackson2JsonDecoder;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeStrategies;
import org.springframework.web.reactive.function.client.WebClient;
public class WebClientConfig {
private ReactorLoadBalancerExchangeFilterFunction lbFunction;
private ObjectMapper objectMapper;
public WebClient webClient(ClientRegistrationRepository clientRegistrations,
OAuth2AuthorizedClientRepository auth2AuthorizedClients){
ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder()
.codecs(configurer ->
configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper)))
.build();
ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 =
new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, auth2AuthorizedClients);
oauth2.setDefaultClientRegistrationId("pricing-client");
return WebClient.builder().apply(oauth2.oauth2Configuration())
.exchangeStrategies(exchangeStrategies)
.filter(lbFunction).baseUrl("http://pricing/pricing/price").build();
}
}
In the code above, we set a custom json decoder, from the objectMapper
that includes the MoneyModule
, so the monetary amounts are correctly serialized and deserialized. Also, we set pricing-client
as the default OAuth 2.0 registrationId
. For service discovery, a ReactorLoadBalancerExchangeFilterFunction
must be added to the WebClient
.
Let’s now configure the OAuth 2.0 client registration. Edit the cart-service application.yml
and add security.oauth2 properties. The cart-service
is a resource server and an OAuth 2.0 client at the same time. The final configuration must be:
xxxxxxxxxx
server
port8081
spring
application
name cart
security
oauth2
resourceserver
jwt
issuer-uri yourOktaIssuer
client
registration
pricing-client
provider okta
authorization-grant-type client_credentials
scope pricing
client-id clientId
client-secret clientSecret
provider
okta
issuer-uri yourOktaIssuer
cloud
loadbalancer
ribbon
enabledfalse
logging
level
com.okta.developer DEBUG
org.springframework.web DEBUG
Note the Ribbon
load balancer has been disabled, otherwise the ReactorLoadBalancer
auto-configuration will fail.
Also, note the requested scope for the client_credentials grant is pricing
, the custom scope. Then, the accessTokens for this client will only have access to the pricing-service
. Adding a custom scope for the client_credentials flow is a best practice.
Spring Boot will auto-configure the application as an OAuth2 client because of the client.registration
presence in the YAML. Add a src/main/java/com/okta/developer/cartservice/WebSecurity.java
class to override the auto-configuration, and configure the application as an OAuth 2.0 resource server:
xxxxxxxxxx
package com.okta.developer.cartservice;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
public class WebSecurity extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
}
}
Create the class src/main/java/com/okta/developer/cartservice/service/PricingException.java
to return HttpStatus.INTERNAL_SERVER_ERROR
(HTTP status 500) when the cart cannot be priced due to an unexpected error.
xxxxxxxxxx
package com.okta.developer.cartservice.service;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
HttpStatus.INTERNAL_SERVER_ERROR) (
public class PricingException extends RuntimeException {
public PricingException(Exception e) {
super(e);
}
}
Add a PricingService for the pricing implementation. Create the class src/main/java/com/okta/developer/cartservice/service/PricingService.java
:
xxxxxxxxxx
package com.okta.developer.cartservice.service;
import com.okta.developer.cartservice.model.Cart;
import com.okta.developer.cartservice.model.LineItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
public class PricingService {
private static final Logger logger = LoggerFactory.getLogger(PricingService.class);
private WebClient webClient;
public Cart price(Cart cart){
try {
Mono<Cart> response = webClient
.post()
.bodyValue(cart)
.retrieve().bodyToMono(Cart.class);
Cart priced = response.block();
for (int i = 0; i < priced.getLineItems().size(); i++) {
LineItem pricedLineItem = priced.getLineItems().get(i);
LineItem lineItem = cart.getLineItems().get(i);
lineItem.setPrice(pricedLineItem.getPrice());
}
cart.setTotal(priced.getTotal());
return cart;
} catch (Exception e){
logger.error("Could not price cart:", e);
throw new PricingException(e);
}
}
}
Note that the ‘WebClient’ is making a synchronous call, as we invoke response.block()
to get the pricing result. This is the expected approach for non-reactive applications.
Modify the CartController
to request pricing when creating a cart:
xxxxxxxxxx
package com.okta.developer.cartservice.controller;
import com.okta.developer.cartservice.model.Cart;
import com.okta.developer.cartservice.repository.CartRepository;
import com.okta.developer.cartservice.service.PricingService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
public class CartController {
private CartRepository repository;
private PricingService pricingService;
"/cart/{id}") (
public Cart getCart( Integer id){
return repository.findById(id).orElseThrow(() -> new CartNotFoundException("Cart not found:" + id));
}
"/cart") (
public Cart saveCart( Cart cart){
Cart priced = pricingService.price(cart);
Cart saved = repository.save(priced);
return saved;
}
}
Restart the cart-service:
xxxxxxxxxx
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_PRICINGCLIENT_CLIENTID={serviceClientId} \
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_PRICINGCLIENT_CLIENTSECRET={serviceClientSecret} \
./mvnw spring-boot:run
Putting it All Together
Create a cart through the API Gateway again, make sure to have a valid accessToken from http://localhost:8080/greeting
:
xxxxxxxxxx
export ACCESS_TOKEN={accessToken}
curl -v\
-d '{"customerId": "uijoon@mail.com", "lineItems": [{ "productName": "jeans", "quantity": 1}]}' \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
http://localhost:8080/cart
You should get a priced cart as response:
xxxxxxxxxx
{
"id":1,
"customerId":"uijoon@example.com",
"total":{
"amount":86.20,
"currency":"USD",
"formatted":"USD86.20"
},
"lineItems":[
{
"id":2,
"productName":"jeans",
"quantity":1,
"price":{
"amount":86.20,
"currency":"USD",
"formatted":"USD86.20"
}
}
]
}
Take a look at the System Log in the Okta Dashboard and you will see an entry indicating the Cart Service requested an access token:
Learn More About Building Secure Applications
In this tutorial, you learned how to create an API Gateway with Spring Cloud Gateway, and how to configure three common OAuth2 patterns (1. code flow, 2. token relay and 3. client credentials grant) using Okta Spring Boot Starter and Spring Security. . You can find all the code at GitHub.
If you like this blog post and want to see more like it, follow@oktadev on Twitter, subscribe to our YouTube channel, or follow us on LinkedIn. As always, please leave a comment below if you have any questions.