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:

Table of Contents

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.

OIDC and OAuth flow

For testing this OAuth2 pattern, create the API Gateway with service discovery. First, create a base folder for all the projects:

Shell
 




xxxxxxxxxx
1


 
1
mkdir oauth2-patterns
2
cd oauth2-patterns



Create a Eureka Discovery Service

With Spring Initializr, create an Eureka server:

Shell
 




xxxxxxxxxx
1


 
1
curl https://start.spring.io/starter.zip -d dependencies=cloud-eureka-server \
2
-d groupId=com.okta.developer \
3
-d artifactId=discovery-service  \
4
-d name="Eureka Service" \
5
-d description="Discovery service" \
6
-d packageName=com.okta.developer.discovery \
7
-d javaVersion=11 \
8
-o eureka.zip



Unzip the file:

Shell
 




xxxxxxxxxx
1


 
1
unzip eureka.zip -d eureka
2
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:

XML
 




xxxxxxxxxx
1


 
1
<dependency>
2
  <groupId>org.glassfish.jaxb</groupId>
3
  <artifactId>jaxb-runtime</artifactId>
4
</dependency>



Edit EurekaServiceApplication to add @EnableEurekaServer annotation:

Java
 




xxxxxxxxxx
1
14


 
1
package com.okta.developer.discovery;
2
 
          
3
import org.springframework.boot.SpringApplication;
4
import org.springframework.boot.autoconfigure.SpringBootApplication;
5
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
6
 
          
7
@SpringBootApplication
8
@EnableEurekaServer
9
public class EurekaServiceApplication {
10
 
          
11
    public static void main(String[] args) {
12
        SpringApplication.run(EurekaServiceApplication.class, args);
13
    }
14
}



Rename src/main/resources/application.properties to application.yml and add the following content:

YAML
 




xxxxxxxxxx
1
11


 
1
server:
2
 port: 8761
3
 
          
4
eureka:
5
 instance:
6
   hostname: localhost
7
 client:
8
   registerWithEureka: false
9
   fetchRegistry: false
10
   serviceUrl:
11
     defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/



Start the service:

Shell
 




xxxxxxxxxx
1


 
1
./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.

Shell
 




xxxxxxxxxx
1


 
1
curl https://start.spring.io/starter.zip \
2
-d dependencies=cloud-eureka,cloud-gateway,webflux,okta,cloud-security,thymeleaf \
3
-d groupId=com.okta.developer \
4
-d artifactId=api-gateway  \
5
-d name="Spring Cloud Gateway Application" \
6
-d description="Demo project of a Spring Cloud Gateway application and OAuth flows" \
7
-d packageName=com.okta.developer.gateway \
8
-d javaVersion=11 \
9
-o api-gateway.zip



Unzip the project:

Shell
 




xxxxxxxxxx
1


 
1
unzip api-gateway.zip -d api-gateway
2
cd api-gateway



Edit SpringCloudGatewayApplication to add @EnableEurekaClient annotation.

Java
 




xxxxxxxxxx
1
14


 
1
package com.okta.developer.gateway;
2
 
          
3
import org.springframework.boot.SpringApplication;
4
import org.springframework.boot.autoconfigure.SpringBootApplication;
5
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
6
 
          
7
@SpringBootApplication
8
@EnableEurekaClient
9
public class SpringCloudGatewayApplication {
10
 
          
11
    public static void main(String[] args) {
12
        SpringApplication.run(SpringCloudGatewayApplication.class, args);
13
    }
14
}



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.

Java
 




xxxxxxxxxx
1
23


 
1
package com.okta.developer.gateway.controller;
2
 
          
3
import org.springframework.security.core.annotation.AuthenticationPrincipal;
4
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
5
import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;
6
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
7
import org.springframework.stereotype.Controller;
8
import org.springframework.ui.Model;
9
import org.springframework.web.bind.annotation.RequestMapping;
10
 
          
11
@Controller
12
public class GreetingController {
13
 
          
14
    @RequestMapping("/greeting")
15
    public String greeting(@AuthenticationPrincipal OidcUser oidcUser, Model model,
16
                           @RegisteredOAuth2AuthorizedClient("okta") OAuth2AuthorizedClient client) {
17
        model.addAttribute("username", oidcUser.getEmail());
18
        model.addAttribute("idToken", oidcUser.getIdToken());
19
        model.addAttribute("accessToken", client.getAccessToken());
20
 
          
21
        return "greeting";
22
    }
23
}



Add a greeting template src/main/resources/templates/greeting.html:

HTML
 




xxxxxxxxxx
1
13


 
1
<!DOCTYPE HTML>
2
<html xmlns:th="https://www.thymeleaf.org">
3
<head>
4
    <title>Greeting</title>
5
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
6
</head>
7
<body>
8
 
          
9
<h1><p th:text="'Hello, ' + ${username} + '!'" ></p></h1>
10
<p th:text="'idToken: ' + ${idToken.tokenValue}" ></p><br/>
11
<p th:text="'accessToken: ' + ${accessToken.tokenValue}" ></p><br>
12
</body>
13
</html>



Rename src/main/resources/application.properties to application.yml and add the following properties:

YAML
 




xxxxxxxxxx
1
12


 
1
spring:
2
 application:
3
   name: gateway
4
 cloud:
5
   gateway:
6
     discovery:
7
       locator:
8
         enabled: true
9
logging:
10
 level:
11
   org.springframework.cloud.gateway: DEBUG
12
   reactor.netty: DEBUG



Add src/main/java/com/okta/developer/gateway/OktaOAuth2WebSecurity.java to make the API Gateway a resource server with login enabled:

Java
 




xxxxxxxxxx
1
23


 
1
package com.okta.developer.gateway;
2
 
          
3
import org.springframework.context.annotation.Bean;
4
import org.springframework.context.annotation.Configuration;
5
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
6
import org.springframework.security.config.web.server.ServerHttpSecurity;
7
import org.springframework.security.web.server.SecurityWebFilterChain;
8
 
          
9
@Configuration
10
@EnableWebFluxSecurity
11
public class OktaOAuth2WebSecurity {
12
 
          
13
    @Bean
14
    public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
15
        http.csrf().disable()
16
                .authorizeExchange()
17
                .anyExchange()
18
                .authenticated()
19
                .and().oauth2Login()
20
                .and().oauth2ResourceServer().jwt();
21
        return http.build();
22
    }
23
}



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

  1. From the Applications page, choose Add Application.
  2. On the Create New Application page, select Web.
  3. 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:

Shell
 




xxxxxxxxxx
1


 
1
OKTA_OAUTH2_ISSUER={yourOktaIssuer} \
2
OKTA_OAUTH2_CLIENT_ID={yourOktaClientId} \
3
OKTA_OAUTH2_CLIENT_SECRET={yourOktaClientSecret} \
4
./mvnw spring-boot:run



Go to http://localhost:8080/greeting. The gateway will redirect to Okta login page:Okta sign in page

After the login, the idToken and accessToken will be displayed in the browser.

YAML
 




xxxxxxxxxx
1


 
1
idToken: eyJraWQiOiIw...
2
 
          
3
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.

API Gateway acting as client

Create a REST API Service

Let’s create a shopping cart service.

Shell
 




xxxxxxxxxx
1


1
curl https://start.spring.io/starter.zip -d dependencies=web,data-jpa,h2,cloud-eureka,okta,security,lombok \
2
-d groupId=com.okta.developer \
3
-d artifactId=cart-service  \
4
-d name="Cart Service" \
5
-d description="Demo cart microservice" \
6
-d packageName=com.okta.developer.cartservice \
7
-d javaVersion=11 \
8
-o cart-service.zip



Unzip the file:

Shell
 




xxxxxxxxxx
1


 
1
unzip cart-service.zip -d cart-service
2
cd cart-service



Edit pom.xml and add Jackson Datatype Money dependency. For this tutorial, we will use JavaMoney for currency.

XML
 




xxxxxxxxxx
1


 
1
<dependency>
2
  <groupId>org.zalando</groupId>
3
  <artifactId>jackson-datatype-money</artifactId>
4
  <version>1.1.1</version>
5
</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:

Java
 




xxxxxxxxxx
1
31


 
1
package com.okta.developer.cartservice.model;
2
 
          
3
import javax.money.CurrencyUnit;
4
import javax.money.Monetary;
5
import javax.money.MonetaryAmount;
6
import javax.persistence.AttributeConverter;
7
import java.math.BigDecimal;
8
 
          
9
public class MonetaryAmountConverter implements AttributeConverter<MonetaryAmount, BigDecimal> {
10
 
          
11
    private final CurrencyUnit USD = Monetary.getCurrency("USD");
12
 
          
13
    @Override
14
    public BigDecimal convertToDatabaseColumn(MonetaryAmount monetaryAmount) {
15
        if (monetaryAmount == null){
16
            return null;
17
        }
18
        return monetaryAmount.getNumber().numberValue(BigDecimal.class);
19
    }
20
 
          
21
    @Override
22
    public MonetaryAmount convertToEntityAttribute(BigDecimal bigDecimal) {
23
        if (bigDecimal == null){
24
            return null;
25
        }
26
        return Monetary.getDefaultAmountFactory()
27
                .setCurrency(USD)
28
                .setNumber(bigDecimal.doubleValue())
29
                          .create();
30
    }
31
}



Create the Cart and LineItem model classes under src/main/java/com/okta/developer/cartservice/model package:

Java
 




xxxxxxxxxx
1
30


 
1
package com.okta.developer.cartservice.model;
2
 
          
3
 
          
4
import lombok.Data;
5
 
          
6
import javax.money.MonetaryAmount;
7
import javax.persistence.*;
8
import java.util.ArrayList;
9
import java.util.List;
10
 
          
11
@Entity
12
@Data
13
public class Cart {
14
 
          
15
    @Id
16
    @GeneratedValue(strategy= GenerationType.AUTO)
17
    private Integer id;
18
 
          
19
    private String customerId;
20
    @Convert(converter=MonetaryAmountConverter.class)
21
    private MonetaryAmount total;
22
 
          
23
    @OneToMany(cascade = CascadeType.ALL)
24
    private List<LineItem> lineItems = new ArrayList<>();
25
 
          
26
 
          
27
    public void addLineItem(LineItem lineItem) {
28
        this.lineItems.add(lineItem);
29
    }
30
}


Java
 




xxxxxxxxxx
1
22


 
1
package com.okta.developer.cartservice.model;
2
 
          
3
import lombok.Data;
4
 
          
5
import javax.money.MonetaryAmount;
6
import javax.persistence.*;
7
 
          
8
@Entity
9
@Data
10
public class LineItem {
11
 
          
12
    @Id
13
    @GeneratedValue(strategy= GenerationType.AUTO)
14
    private Integer id;
15
 
          
16
    private String productName;
17
 
          
18
    private Integer quantity;
19
 
          
20
    @Convert(converter=MonetaryAmountConverter.class)
21
    private MonetaryAmount price;
22
}



Add a CartRepository under src/main/java/com/okta/developer/cartservice/repository/CartRepository.java:

Java
 




xxxxxxxxxx
1


 
1
package com.okta.developer.cartservice.repository;
2
 
          
3
import com.okta.developer.cartservice.model.Cart;
4
import org.springframework.data.repository.CrudRepository;
5
 
          
6
public interface CartRepository extends CrudRepository<Cart, Integer> {
7
}



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:

Java
 




xxxxxxxxxx
1
12


 
1
package com.okta.developer.cartservice.controller;
2
 
          
3
import org.springframework.http.HttpStatus;
4
import org.springframework.web.bind.annotation.ResponseStatus;
5
 
          
6
@ResponseStatus(HttpStatus.NOT_FOUND)
7
public class CartNotFoundException extends RuntimeException {
8
 
          
9
    public CartNotFoundException(String message) {
10
        super(message);
11
    }
12
}



Add a CartController under src/main/java/com/okta/developer/cartservice/controller/CartController.java:

Java
 




xxxxxxxxxx
1
25


 
1
package com.okta.developer.cartservice.controller;
2
 
          
3
import com.okta.developer.cartservice.model.Cart;
4
import com.okta.developer.cartservice.repository.CartRepository;
5
import org.springframework.beans.factory.annotation.Autowired;
6
import org.springframework.web.bind.annotation.*;
7
 
          
8
@RestController
9
public class CartController {
10
 
          
11
    @Autowired
12
    private CartRepository repository;
13
 
          
14
    @GetMapping("/cart/{id}")
15
    public Cart getCart(@PathVariable Integer id){
16
        return repository.findById(id).orElseThrow(() -> new CartNotFoundException("Cart not found:" + id));
17
    }
18
 
          
19
    @PostMapping("/cart")
20
    public Cart saveCart(@RequestBody  Cart cart){
21
 
          
22
        Cart saved = repository.save(cart);
23
        return saved;
24
    }
25
}



Configure the Jackson Money Datatype module. Add a WebConfig class under src/main/java/com/okta/developer/cartservice:

Java
 




xxxxxxxxxx
1
15


 
1
package com.okta.developer.cartservice;
2
 
          
3
import org.springframework.context.annotation.Bean;
4
import org.springframework.context.annotation.Configuration;
5
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
6
import org.zalando.jackson.datatype.money.MoneyModule;
7
 
          
8
@Configuration
9
public class WebConfig implements WebMvcConfigurer {
10
 
          
11
    @Bean
12
    public MoneyModule moneyModule(){
13
        return new MoneyModule().withDefaultFormatting();
14
    }
15
}



Edit CartServiceApplication and add @EnableEurekaClient:

Java
 




xxxxxxxxxx
1
15


 
1
package com.okta.developer.cartservice;
2
 
          
3
import org.springframework.boot.SpringApplication;
4
import org.springframework.boot.autoconfigure.SpringBootApplication;
5
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
6
 
          
7
@SpringBootApplication
8
@EnableEurekaClient
9
public class CartServiceApplication {
10
 
          
11
    public static void main(String[] args) {
12
        SpringApplication.run(CartServiceApplication.class, args);
13
    }
14
 
          
15
}


Rename src/main/resources/application.propeties to application.yml and add the following values:

YAML
 




xxxxxxxxxx
1
11


 
1
server:
2
 port: 8081
3
 
          
4
spring:
5
 application:
6
   name: cart
7
 
          
8
okta:
9
 oauth2:
10
   issuer: {yourOktaIssuer}
11
   audience: api://default



Start the cart-service:

Shell
 




xxxxxxxxxx
1


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

Java
 




xxxxxxxxxx
1
27


 
1
package com.okta.developer.gateway;
2
 
          
3
import org.springframework.boot.SpringApplication;
4
import org.springframework.boot.autoconfigure.SpringBootApplication;
5
import org.springframework.cloud.gateway.route.RouteLocator;
6
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
7
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
8
import org.springframework.cloud.security.oauth2.gateway.TokenRelayGatewayFilterFactory;
9
import org.springframework.context.annotation.Bean;
10
 
          
11
@SpringBootApplication
12
@EnableEurekaClient
13
public class SpringCloudGatewayApplication {
14
 
          
15
    public static void main(String[] args) {
16
        SpringApplication.run(SpringCloudGatewayApplication.class, args);
17
    }
18
 
          
19
    @Bean
20
    public RouteLocator routeLocator(RouteLocatorBuilder builder, TokenRelayGatewayFilterFactory filterFactory) {
21
        return builder.routes()
22
                .route("cart", r -> r.path("/cart/**")
23
                        .filters(f -> f.filter(filterFactory.apply()))
24
                        .uri("lb://cart"))
25
                .build();
26
    }
27
}



TokenRelayGatewayFilterFactory will find the accessToken from the registered OAuth2 client and include it in the outbound cart request.

Restart the gateway with:

Shell
 




xxxxxxxxxx
1


 
1
OKTA_OAUTH2_ISSUER={yourOktaIssuer} \
2
OKTA_OAUTH2_CLIENT_ID={clientId} \
3
OKTA_OAUTH2_CLIENT_SECRET={clientSecret} \
4
./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.

Shell
 




xxxxxxxxxx
1
15


 
1
export ACCESS_TOKEN={accessToken}
2
 
          
3
# Add an item to the cart
4
curl \
5
  -d '{"customerId": "uijoon@example.com", "lineItems": [{ "productName": "jeans", "quantity": 1}]}' \
6
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
7
  -H 'Content-Type: application/json' \
8
  -H 'Accept: application/json' \
9
  http://localhost:8080/cart
10
 
          
11
# Return the contents of the cart
12
curl \
13
  -H 'Accept: application/json' \
14
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
15
  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:

Shell
 




xxxxxxxxxx
1


 
1
curl https://start.spring.io/starter.zip -d dependencies=web,cloud-eureka,okta,security,lombok \
2
-d groupId=com.okta.developer \
3
-d artifactId=pricing-service  \
4
-d name="Pricing Service" \
5
-d description="Demo pricing microservice" \
6
-d packageName=com.okta.developer.pricing \
7
-d javaVersion=11 \
8
-o pricing-service.zip



Unzip the file:

Shell
 




xxxxxxxxxx
1


 
1
unzip pricing-service.zip -d pricing-service
2
cd pricing-service



Edit pom.xml and add Jackson Datatype Money dependency again.

XML
 




xxxxxxxxxx
1


 
1
<dependency>
2
  <groupId>org.zalando</groupId>
3
  <artifactId>jackson-datatype-money</artifactId>
4
  <version>1.1.1</version>
5
</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:

Java
 




xxxxxxxxxx
1
19


 
1
package com.okta.developer.pricing.model;
2
 
          
3
import lombok.Data;
4
import javax.money.MonetaryAmount;
5
import java.util.ArrayList;
6
import java.util.List;
7
 
          
8
@Data
9
public class Cart {
10
 
          
11
    private Integer id;
12
    private String customerId;
13
    private List<LineItem> lineItems = new ArrayList<>();
14
    private MonetaryAmount total;
15
 
          
16
    public void addLineItem(LineItem lineItem){
17
        this.lineItems.add(lineItem);
18
    }
19
}


Java
 




xxxxxxxxxx
1
21


 
1
package com.okta.developer.pricing.model;
2
 
          
3
import lombok.Data;
4
import javax.money.MonetaryAmount;
5
 
          
6
@Data
7
public class LineItem {
8
 
          
9
    private Integer id;
10
    private Integer quantity;
11
    private MonetaryAmount price;
12
    private String productName;
13
 
          
14
    public LineItem(){
15
    }
16
 
          
17
    public LineItem(Integer id, Integer quantity) {
18
        this.id = id;
19
        this.quantity = quantity;
20
    }
21
}



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.

Java
 




xxxxxxxxxx
1


 
1
package com.okta.developer.pricing.service;
2
 
          
3
import com.okta.developer.pricing.model.Cart;
4
 
          
5
public interface PricingService {
6
 
          
7
    Cart price(Cart cart);
8
}


Java
 




xxxxxxxxxx
1
41


 
1
package com.okta.developer.pricing.service;
2
 
          
3
import com.okta.developer.pricing.model.Cart;
4
import com.okta.developer.pricing.model.LineItem;
5
import org.springframework.stereotype.Service;
6
 
          
7
import javax.money.CurrencyUnit;
8
import javax.money.Monetary;
9
import javax.money.MonetaryAmount;
10
import java.math.BigDecimal;
11
import java.math.RoundingMode;
12
 
          
13
@Service
14
public class DefaultPricingService implements PricingService {
15
 
          
16
    private final CurrencyUnit USD = Monetary.getCurrency("USD");
17
 
          
18
    @Override
19
    public Cart price(Cart cart) {
20
 
          
21
        MonetaryAmount total = Monetary.getDefaultAmountFactory()
22
                .setCurrency(USD)
23
                .setNumber(0)
24
                .create();
25
 
          
26
        for (LineItem li : cart.getLineItems()) {
27
            BigDecimal bigDecimal = new BigDecimal(Math.random() * 100)
28
                    .setScale(2, RoundingMode.HALF_UP);
29
 
          
30
            MonetaryAmount amount = Monetary.getDefaultAmountFactory()
31
                    .setCurrency(USD)
32
                    .setNumber(bigDecimal)
33
                    .create();
34
            li.setPrice(amount);
35
            total = total.add(amount.multiply(li.getQuantity()));
36
        }
37
 
          
38
        cart.setTotal(total);
39
        return cart;
40
    }
41
}



Create a PricingController to handle the pricing request. Add the class src/main/java/com/okta/developer/pricing/controller/PricingController.java:

Java
 




xxxxxxxxxx
1
22


 
1
package com.okta.developer.pricing.controller;
2
 
          
3
import com.okta.developer.pricing.model.Cart;
4
import com.okta.developer.pricing.service.PricingService;
5
import org.springframework.beans.factory.annotation.Autowired;
6
import org.springframework.web.bind.annotation.PostMapping;
7
import org.springframework.web.bind.annotation.RequestBody;
8
import org.springframework.web.bind.annotation.RestController;
9
 
          
10
@RestController
11
public class PricingController {
12
 
          
13
    @Autowired
14
    private PricingService pricingService;
15
 
          
16
    @PostMapping("/pricing/price")
17
    public Cart price(@RequestBody Cart cart){
18
 
          
19
        Cart priced = pricingService.price(cart);
20
        return priced;
21
    }
22
}



Configure the Jackson Money Module in a src/main/java/com/okta/developer/pricing/WebConfig.java class:

Java
 




xxxxxxxxxx
1
15


 
1
package com.okta.developer.pricing;
2
 
          
3
import org.springframework.context.annotation.Bean;
4
import org.springframework.context.annotation.Configuration;
5
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
6
import org.zalando.jackson.datatype.money.MoneyModule;
7
 
          
8
@Configuration
9
public class WebConfig implements WebMvcConfigurer {
10
 
          
11
    @Bean
12
    public MoneyModule moneyModule(){
13
        return new MoneyModule().withDefaultFormatting();
14
    }
15
}


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:

Java
 




xxxxxxxxxx
1
19


 
1
package com.okta.developer.pricing;
2
 
          
3
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
4
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
5
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
6
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
7
 
          
8
@EnableWebSecurity
9
public class WebSecurity extends WebSecurityConfigurerAdapter {
10
 
          
11
    protected void configure(HttpSecurity http) throws Exception {
12
        http
13
                .authorizeRequests(authorizeRequests -> authorizeRequests
14
                        .mvcMatchers("/pricing/**").hasAuthority("SCOPE_pricing")
15
                        .anyRequest().authenticated()
16
                )
17
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
18
    }
19
}



Add @EnableEurekaClient to PricingServiceApplication:

Java
 




xxxxxxxxxx
1
14


 
1
package com.okta.developer.pricing;
2
 
          
3
import org.springframework.boot.SpringApplication;
4
import org.springframework.boot.autoconfigure.SpringBootApplication;
5
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
6
 
          
7
@SpringBootApplication
8
@EnableEurekaClient
9
public class PricingServiceApplication {
10
 
          
11
    public static void main(String[] args) {
12
        SpringApplication.run(PricingServiceApplication.class, args);
13
    }
14
}



Rename src/main/resources/application.properties to application.yml and add the following:

YAML
 




xxxxxxxxxx
1
11


 
1
server:
2
 port: 8082
3
 
          
4
spring:
5
 application:
6
   name: pricing
7
 
          
8
okta:
9
 oauth2:
10
   issuer: {yourOktaIssuer}
11
   audience: api://default



Start the service:

Shell
 




xxxxxxxxxx
1


 
1
./mvnw spring-boot:run



Let’s try the pricing API without an accessToken:

Shell
 




xxxxxxxxxx
1


 
1
curl -v\
2
  -d '{"customerId": "uijoon@mail.com", "lineItems": [{ "productName": "jeans", "quantity": 1}]}' \
3
  -H 'Content-Type: application/json' \
4
  -H 'Accept: application/json' \
5
  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.

  1. From the Applications page, choose Add Application
  2. On the Create New Application page, select Service
  3. 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.


Add scope

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:

XML
 




xxxxxxxxxx
1


 
1
<dependency>
2
    <groupId>org.springframework.boot</groupId>
3
    <artifactId>spring-boot-starter-webflux</artifactId>
4
</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:

Java
 




xxxxxxxxxx
1
42


 
1
package com.okta.developer.cartservice;
2
 
          
3
import com.fasterxml.jackson.databind.ObjectMapper;
4
import org.springframework.beans.factory.annotation.Autowired;
5
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction;
6
import org.springframework.context.annotation.Bean;
7
import org.springframework.context.annotation.Configuration;
8
import org.springframework.http.codec.json.Jackson2JsonDecoder;
9
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
10
import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository;
11
import org.springframework.security.oauth2.client.web.reactive.function.client.ServletOAuth2AuthorizedClientExchangeFilterFunction;
12
import org.springframework.web.reactive.function.client.ExchangeStrategies;
13
import org.springframework.web.reactive.function.client.WebClient;
14
 
          
15
@Configuration
16
public class WebClientConfig {
17
 
          
18
    @Autowired
19
    private ReactorLoadBalancerExchangeFilterFunction lbFunction;
20
 
          
21
    @Autowired
22
    private ObjectMapper objectMapper;
23
 
          
24
    @Bean
25
    public WebClient webClient(ClientRegistrationRepository clientRegistrations,
26
                               OAuth2AuthorizedClientRepository auth2AuthorizedClients){
27
 
          
28
        ExchangeStrategies exchangeStrategies = ExchangeStrategies.builder()
29
                .codecs(configurer ->
30
                        configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper)))
31
                .build();
32
 
          
33
        ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 =
34
                new ServletOAuth2AuthorizedClientExchangeFilterFunction(clientRegistrations, auth2AuthorizedClients);
35
 
          
36
        oauth2.setDefaultClientRegistrationId("pricing-client");
37
 
          
38
        return WebClient.builder().apply(oauth2.oauth2Configuration())
39
                .exchangeStrategies(exchangeStrategies)
40
                .filter(lbFunction).baseUrl("http://pricing/pricing/price").build();
41
    }
42
}



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:

YAML
 




xxxxxxxxxx
1
31


 
1
server:
2
 port: 8081
3
 
          
4
spring:
5
 application:
6
   name: cart
7
 security:
8
   oauth2:
9
     resourceserver:
10
       jwt:
11
         issuer-uri: {yourOktaIssuer}
12
     client:
13
       registration:
14
         pricing-client:
15
           provider: okta
16
           authorization-grant-type: client_credentials
17
           scope: pricing
18
           client-id: {clientId}
19
           client-secret: {clientSecret}
20
       provider:
21
         okta:
22
           issuer-uri: {yourOktaIssuer}
23
 cloud:
24
   loadbalancer:
25
     ribbon:
26
       enabled: false
27
 
          
28
logging:
29
 level:
30
   com.okta.developer: DEBUG
31
   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:

Java
 




xxxxxxxxxx
1
18


 
1
package com.okta.developer.cartservice;
2
 
          
3
import org.springframework.context.annotation.Configuration;
4
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
5
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
6
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
7
 
          
8
@Configuration
9
public class WebSecurity extends WebSecurityConfigurerAdapter {
10
 
          
11
    protected void configure(HttpSecurity http) throws Exception {
12
        http
13
                .authorizeRequests(authorize -> authorize
14
                        .anyRequest().authenticated()
15
                )
16
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
17
    }
18
}



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.

Java
 




xxxxxxxxxx
1
12


 
1
package com.okta.developer.cartservice.service;
2
 
          
3
import org.springframework.http.HttpStatus;
4
import org.springframework.web.bind.annotation.ResponseStatus;
5
 
          
6
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
7
public class PricingException extends RuntimeException {
8
 
          
9
    public PricingException(Exception e) {
10
        super(e);
11
    }
12
}



Add a PricingService for the pricing implementation. Create the class src/main/java/com/okta/developer/cartservice/service/PricingService.java:

Java
 




xxxxxxxxxx
1
45


 
1
package com.okta.developer.cartservice.service;
2
 
          
3
import com.okta.developer.cartservice.model.Cart;
4
import com.okta.developer.cartservice.model.LineItem;
5
import org.slf4j.Logger;
6
import org.slf4j.LoggerFactory;
7
import org.springframework.beans.factory.annotation.Autowired;
8
import org.springframework.stereotype.Service;
9
import org.springframework.web.reactive.function.client.WebClient;
10
import reactor.core.publisher.Mono;
11
 
          
12
@Service
13
public class PricingService {
14
 
          
15
    private static final Logger logger = LoggerFactory.getLogger(PricingService.class);
16
 
          
17
    @Autowired
18
    private WebClient webClient;
19
 
          
20
    public Cart price(Cart cart){
21
        try {
22
 
          
23
            Mono<Cart> response = webClient
24
                    .post()
25
                    .bodyValue(cart)
26
                    .retrieve().bodyToMono(Cart.class);
27
 
          
28
            Cart priced = response.block();
29
 
          
30
            for (int i = 0; i < priced.getLineItems().size(); i++) {
31
                LineItem pricedLineItem = priced.getLineItems().get(i);
32
                LineItem lineItem = cart.getLineItems().get(i);
33
                lineItem.setPrice(pricedLineItem.getPrice());
34
            }
35
 
          
36
            cart.setTotal(priced.getTotal());
37
 
          
38
 
          
39
            return cart;
40
        } catch (Exception e){
41
            logger.error("Could not price cart:", e);
42
            throw new PricingException(e);
43
        }
44
    }
45
}



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:

Java
 




xxxxxxxxxx
1
31


 
1
package com.okta.developer.cartservice.controller;
2
 
          
3
import com.okta.developer.cartservice.model.Cart;
4
import com.okta.developer.cartservice.repository.CartRepository;
5
import com.okta.developer.cartservice.service.PricingService;
6
import org.springframework.beans.factory.annotation.Autowired;
7
import org.springframework.web.bind.annotation.*;
8
 
          
9
@RestController
10
public class CartController {
11
 
          
12
    @Autowired
13
    private CartRepository repository;
14
 
          
15
    @Autowired
16
    private PricingService pricingService;
17
 
          
18
 
          
19
    @GetMapping("/cart/{id}")
20
    public Cart getCart(@PathVariable Integer id){
21
        return repository.findById(id).orElseThrow(() -> new CartNotFoundException("Cart not found:" + id));
22
    }
23
 
          
24
    @PostMapping("/cart")
25
    public Cart saveCart(@RequestBody  Cart cart){
26
 
          
27
        Cart priced = pricingService.price(cart);
28
        Cart saved = repository.save(priced);
29
        return saved;
30
    }
31
}



Restart the cart-service:

Shell
 




xxxxxxxxxx
1


 
1
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_PRICINGCLIENT_CLIENTID={serviceClientId} \
2
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_PRICINGCLIENT_CLIENTSECRET={serviceClientSecret} \
3
./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:

Shell
 




xxxxxxxxxx
1


 
1
export ACCESS_TOKEN={accessToken}
2
 
          
3
curl -v\
4
  -d '{"customerId": "uijoon@mail.com", "lineItems": [{ "productName": "jeans", "quantity": 1}]}' \
5
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
6
  -H 'Content-Type: application/json' \
7
  -H 'Accept: application/json' \
8
  http://localhost:8080/cart



You should get a priced cart as response:

JSON
 




xxxxxxxxxx
1
21


 
1
{
2
   "id":1,
3
   "customerId":"uijoon@example.com",
4
   "total":{
5
      "amount":86.20,
6
      "currency":"USD",
7
      "formatted":"USD86.20"
8
   },
9
   "lineItems":[
10
      {
11
         "id":2,
12
         "productName":"jeans",
13
         "quantity":1,
14
         "price":{
15
            "amount":86.20,
16
            "currency":"USD",
17
            "formatted":"USD86.20"
18
         }
19
      }
20
   ]
21
}



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:

System log

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.


 

 

 

 

Top