Quarkus and OAuth2 (part 2)
Second part of the article about securing communication between microservices with Quarkus.
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.
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.