Quarkus and OAuth2 (part 2)

Introduction

In the first part, we have secured the Stock service API using JWT validation, now we gonna see how the Order service can get a token (step 1) and request Stock service API (step 2).

The code base is available here: jami-quarkus-order-service

Start the service

To start the application in development mode (with hot reload), launch the following command:
./mvnw package quarkus:dev

Quarkus extension Description
Rest Client MicroProfile Rest Client Implementation
OIDC Client Filter Filter to handle JWT

Call the Stock service

Quarkus uses MicroProfile Rest Client, you just have to create an interface with annotations to specify the target Rest service (here the Stock API).

package org.jami.order.service.rest;

import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.GET;
import javax.ws.rs.Path;

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.jami.order.model.ProductStock;
import org.jboss.resteasy.annotations.jaxrs.PathParam;

import io.quarkus.oidc.client.filter.OidcClientFilter;

@RegisterRestClient(configKey = "stock-api")
@OidcClientFilter
@ApplicationScoped
public interface StockRestService {

	@GET
	@Path("/{productSku}")
	ProductStock getStock(@PathParam String productSku);

}
  • @RegisterRestClient instanciates a Rest Client according to configuration (application.properties)
  • @OidcClientFilter handles JWT and add it to Authorization header

As you have may noticed, we use the same annotations (@GET and @Path) than Rest endpoints we have exposed in Stock service.

Rest Client configuration

To configure our Rest client, we have to specify the target url (here the Stock service endpoint):

%dev.org.jami.order.service.rest.StockRestService/mp-rest/url = http://localhost:8082/stocks
org.jami.order.service.StockRestService/mp-rest/scope = javax.inject.ApplicationScoped

Oidc Client Filter configuration

Oidc Client Filter needs to know how to get a token from Authorization server before send request to Stock service.
Here the parameters required to request a token:

%dev.quarkus.oidc-client.auth-server-url=http://localhost:8083/auth/realms/quarkus/
%dev.quarkus.oidc-client.client-id=order-service
%dev.quarkus.oidc-client.credentials.client-secret.value=secret
%dev.quarkus.oidc-client.credentials.client-secret.method=post
%dev.quarkus.oidc-client.grant.type=client
  • grant.type is the OAuth2 flow we want to use. "client" means client_credentials which is the flow for "system to system" authentication.
  • method configure the HTTP Post method to get the token.
client-secret value should not be directly inject in this file for security reason. Instead you can set an environment variable like this: %dev.quarkus.oidc-client.credentials.client-secret.value=${SECRET_VALUE}

Use the StockRestService

StockRestService is injected like any bean using CDI and @Inject annotation.

package org.jami.order.service.impl;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;

import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.jami.order.model.Order;
import org.jami.order.model.ProductStock;
import org.jami.order.service.OrderService;
import org.jami.order.service.rest.StockRestService;

@ApplicationScoped
public class OrderServiceImpl implements OrderService {

	private final StockRestService stockRestService;

	@Inject
	public OrderServiceImpl(@RestClient final StockRestService stockRestService) {
		this.stockRestService = stockRestService;
	}

	@Override
	public void createOrder(Order order) {
		this.checkStocks(order);
	}

	private void checkStocks(Order order) {
		order.getLines().stream().forEach(line -> {
			ProductStock productStock = this.stockRestService.getStock(line.getSku());
			if (productStock.getQuantity() < line.getQuantity()) {
				throw new IllegalStateException("Stock is insufficient for the product " + line.getSku());
			}
		});

	}

}

Testing

Like we did in the first part, we usequarkus-junit5 extension and also quarkus-junit5-mockito for CDI mock support.

package org.jami.order.service.rest;

import java.util.Arrays;

import javax.inject.Inject;

import org.jami.order.model.Line;
import org.jami.order.model.Order;
import org.jami.order.model.ProductStock;
import org.jami.order.service.OrderService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectMock;

@QuarkusTest
public class OrderServiceTest {

	@Inject
	private OrderService orderService;

	@InjectMock
	private StockRestService stockRestService;

	@Test
	public void testCreateOrderWithStocks() {
		Mockito.when(stockRestService.getStock("1234")).thenReturn(new ProductStock("1234", 10));
		final var order = new Order();
		final var line = new Line();
		line.setQuantity(9);
		line.setSku("1234");
		order.setLines(Arrays.asList(line));
		this.orderService.createOrder(order);
	}

	@Test
	public void testCreateOrderWithoutStocks() {
		Mockito.when(stockRestService.getStock("1234")).thenReturn(new ProductStock("1234", 5));
		final var order = new Order();
		final var line = new Line();
		line.setQuantity(9);
		line.setSku("1234");
		order.setLines(Arrays.asList(line));
		Throwable exception = Assertions.assertThrows(IllegalStateException.class,
				() -> this.orderService.createOrder(order));
		Assertions.assertEquals("Stock is insufficient for the product 1234", exception.getMessage());
	}

}

Thanks to @InjectMock annotation, StockRestService is automatically mocked and injected in our OrderService.

Build

This step is similar to the first part we did with Stock service.

Conclusion

Using the Microprofile Rest Client along with the Quarkus OIDC client filter, we don't have to write boilerplate code to get a token and inject it in our HTTP requests.
In addition, it gives a very flexible way to configure the service according to the environment.