8 The web stack

This chapter covers

  • The construction of an edge service and a public API
  • The Vert.x web client
  • JSON web tokens (JWT) and cross-origin resource sharing (CORS)
  • Serving and integrating a Vue.js reactive application with Vert.x
  • Testing an HTTP API with REST Assured

Reactive applications often use HTTP because it is such a versatile protocol, and Vert.x offers comprehensive support for web technologies. The Vert.x web stack provides many tools for building web application backends. These include advanced routing, authentication, an HTTP client, and more. This chapter will guide you through exposing an HTTP API with JSON web tokens (JWT) for access control, making HTTP requests to other services, and building a reactive single-page application that connects to the HTTP API.

Note This book does not cover the following noteworthy elements from the Vert.x web stack that are not needed to build the application in this part of the book: routing with regular expressions, cookies, server-side sessions, server-side template rendering, and cross-site request forgery protection. You can get more details about those topics in the official documentation at https://vertx.io/.

8.1 Exposing a public API

Let’s start with a reminder of what the public API service does, as illustrated in figure 8.1. This service is an edge service (or service gateway, depending on how you prefer to name it) as it exposes an HTTP API, but it essentially composes functionality found in other services. In this case, the user profile and activity services are being used. These two services are internal to the application and are not publicly exposed. They also lack any form of authentication and access control, which is something the public API cannot afford for most operations.

Figure 8.1 Public API overview

The following Vert.x modules are needed to implement the public API:

  • vertx-web, to provide advanced HTTP request-processing functionality

  • vertx-web-client, to issue HTTP requests to the user profile and activity services

  • vertx-auth-jwt, to generate and process JSON web tokens and perform access control

The complete source code of the public API service can be found in the part2-steps-challenge/public-api folder of the book’s source code repository.

We’ll start with the Vert.x web router.

8.1.1 Routing HTTP requests

Vert.x core provides a very low-level HTTP server API, where you need to pass a request handler for all types of HTTP requests. If you just use Vert.x core, you need to manually check the requested path and method. This is fine for simple cases, and it’s what we did in some earlier chapters, but it can quickly become complicated.

The vertx-web module provides a router that can act as a Vert.x HTTP server request handler, and that manages the dispatch of HTTP requests to suitable handlers based on request paths (e.g., /foo) and HTTP methods (e.g., POST). This is illustrated in figure 8.2.


Figure 8.2 Routing HTTP requests

The following listing shows how to initialize and then set up a router as an HTTP request handler.

Listing 8.1 Initializing and using a router as an HTTP request handler

Router router = Router.router(vertx);
// (...)                               

vertx.createHttpServer()
  .requestHandler(router)              
  .listen(8080);

Define routes

A router is just another HTTP request handler.

The Router class provides a fluent API to describe routes based on HTTP methods and paths, as shown in the following listing.

Listing 8.2 Defining routes

BodyHandler bodyHandler = BodyHandler.create();               
router.post().handler(bodyHandler);                           
router.put().handler(bodyHandler);

String prefix = "/api/v1";

router.post(prefix + "/register").handler(this::register);    
router.post(prefix + "/token").handler(this::token);
// (...) defines jwtHandler, more later

router.get(prefix + "/:username/:year/:month")                
  .handler(jwtHandler)                                        
  .handler(this::checkUser)
  .handler(this::monthlySteps);
// (...)

BodyHandler is a predefined handler that extracts HTTP request body payloads.

Here bodyHandler is called for all HTTP POST and PUT requests.

The register method handles /api/v1/register POST requests.

We can extract path parameters by prefixing elements with ":".

Handlers can be chained.

An interesting property of the Vert.x router is that handlers can be chained. With the definitions from listing 8.2, a POST request to /api/v1/register first goes through a BodyHandler instance. This handler is useful for easily decoding an HTTP request body payload. The next handler is the register method.

Listing 8.2 also defines the route for GET requests to monthlySteps, where the request first goes through jwtHandler, and then checkUser, as illustrated in figure 8.3. This is useful for decomposing an HTTP request, processing concerns in several steps: jwtHandler checks that a valid JWT token is in the request, checkUser checks that the JWT token grants permissions to access the resource, and monthlySteps checks how many steps a user has taken in a month.

Figure 8.3 Routing chain for the monthly steps endpoint

Note that both checkUser and jwtHandler will be discussed in section 8.2.

tip The io.vertx.ext.web.handler package contains useful utility handlers including BodyHandler. It especially provides handlers for HTTP authentication, CORS, CSRF, favicon, HTTP sessions, static files serving, virtual hosts, and template rendering.

8.1.2 Making HTTP requests

Let’s now dive into the implementation of a handler. Since the public API service forwards requests to the user profile and activity services, we need to use the Vert.x web client to make HTTP requests. As noted previously, the Vert.x core APIs offer a low-level HTTP client, whereas the WebClient class from the vertx-web-client module offers a richer API.

Creating a web client instance is as simple as this:

WebClient webClient = WebClient.create(vertx);

A WebClient instance is typically stored in a private field of a verticle class, as it can be used to perform multiple concurrent HTTP requests. The whole application uses the RxJava 2 bindings, so we can take advantage of them to compose asynchronous operations. As you will see in later examples, the RxJava bindings sometimes bring additional functionality for dealing with error management.

The following listing shows the implementation of the register route handler.

Listing 8.3 Using the Vert.x web client in a route handler

private void register(RoutingContext ctx) {
  webClient
    .post(3000, "localhost", "/register")                       
    .putHeader("Content-Type", "application/json")              
    .rxSendJson(ctx.getBodyAsJson())                            
    .subscribe(
      response -> sendStatusCode(ctx, response.statusCode()),   
      err -> sendBadGateway(ctx, err));
}

private void sendStatusCode(RoutingContext ctx, int code) {
  ctx.response().setStatusCode(code).end();
}

private void sendBadGateway(RoutingContext ctx, Throwable err) {
  logger.error("Woops", err);
  ctx.fail(502);
}

Methods match the HTTP methods (GET, POST, etc.).

HTTP headers can be passed.

This converts the request from a Vert.x Buffer to a JsonObject.

Subscription in RxJava triggers the request.

This example demonstrates both how to handle an HTTP request with a router, and how to use the web client. The RoutingContext class encapsulates details about the HTTP request and provides the HTTP response object via the response method. HTTP headers can be set in both requests and responses, and the response is sent once the end method has been called. A status code can be specified, although by default it will be 200 (OK).

You can see that getBodyAsJson transforms the HTTP request body to a JsonObject, while rxSendJson sends an HTTP request with a JsonObject as the body. By default, Vert.x Buffer objects carry bodies in both requests and responses, but there are helper methods to convert from or to String, JsonObject, and JsonArray.

The next listing offers another router handler method for HTTP GET requests to /api/v1/:username, where :username is a path parameter.

Listing 8.4 Fetching and forwarding a user’s details

private void fetchUser(RoutingContext ctx) {
  webClient
    .get(3000, "localhost", "/" + ctx.pathParam("username"))      
    .as(BodyCodec.jsonObject())                                   
    .rxSend()
    .subscribe(
      resp -> forwardJsonOrStatusCode(ctx, resp),
      err -> sendBadGateway(ctx, err));
}

private void forwardJsonOrStatusCode(RoutingContext ctx, 
 HttpResponse<JsonObject> resp) {
  if (resp.statusCode() != 200) {
    sendStatusCode(ctx, resp.statusCode());
  } else {
    ctx.response()
      .putHeader("Content-Type", "application/json")
      .end(resp.body().encode());                                 
  }
}

Extracts a path parameter

Converts the response to a JsonObject

Ends the response with some content

This example shows the as method that converts HTTP responses to a type other than Buffer using a BodyCodec. You can also see that the HTTP response’s end method can take an argument that is the response content. It can be a String or a Buffer. While it is often the case that the response is sent in a single end method call, you can send intermediary fragments using the write method until a final end call closes the HTTP response, as shown here:

response.write("hello").write(" world").end();

8.2 Access control with JWT tokens

JSON Web Token (JWT) is an open specification for securely transmitting JSON-encoded data between parties (https://tools.ietf.org/html/rfc7519). JWT tokens are signed with either a symmetric shared secret or an asymmetric public/private key pair, so it is always possible to verify that the information that they contain has not been modified. This is very interesting, as a JWT token can be used to hold claims such as identity and authorization grants. JWT tokens can be exchanged as part of HTTP requests using the Authorization HTTP header.

Let’s look at how to use JWT tokens, what data they contain, and how to both validate and issue them with Vert.x.

tip JWT is only one protocol supported by Vert.x. Vert.x offers the vertx-auth-oauth2 module for OAuth2, which is a popular protocol among public service providers like Google, GitHub, and Twitter. You will be interested in using it if your application needs to integrate with such services (such as when accessing a user’s Gmail account data), or when your application wants to grant third-party access through OAuth2.

8.2.1 Using JWT tokens

To illustrate using JWT tokens, let’s interact with the public API and authenticate as user foo with password 123, and get a JWT token. The following listing shows the HTTP response.

Listing 8.5 Getting a JWT token

$ http :4000/api/v1/token username=foo password=123      
HTTP/1.1 200 OK
Content-Type: application/jwt
content-length: 496

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJkZXZpY2VJZCI6ImExYjIiLCJpYXQiOjE1NjUx
 Njc0NzUsImV4cCI6MTU2NTc3MjI3NSwiaXNzIjoiMTBrLXN0ZXBzLWFwaSIsInN1YiI6ImZvb
 yJ9.J_tn2BjMNYE6eFSHwSJ9e8DoCEUr_xMSlYAyBSy1-E_pouvDq4lp8QjG51cJoa5Gbrt1bg
 tDHinJsLncG1RIsGr_cz1rQw8_GlI_-GdhqFBw8dVjlsgykSf5tfaiiRwORmz7VH_AAk-935aV
 lxMg4mxkbOvN4YDxRLhLb4Y78TA47F__ivNsM4gLD8CHzOUmTEta_pjpZGzsErmYvzDOV6F7rO
 ZcRhZThJxLvR3zskrtx83iaNHTwph53bkHNOQzC66wxNMar_T4HMRWzqnrr-sFIcOwLFsWJKow
 c1rQuadjv-ew541YQLaVmkEcai6leZLwCfCTcsxMX9rt0AmOFg

Authenticating as user foo with password 123

A JWT token has the MIME type application/jwt, which is plain text. We can pass the token to make a request as follows.

Listing 8.6 Using a JWT token to access a resource

http :4000/api/v1/foo Authorization:'Bearer 
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJkZXZpY2VJZCI6ImExYjIiLCJpYXQiOjE1NjUx Njc0NzUsImV4cCI6MTU2NTc3MjI3NSwiaXNzIjoiMTBrLXN0ZXBzLWFwaSIsInN1YiI6ImZvb
 yJ9.J_tn2BjMNYE6eFSHwSJ9e8DoCEUr_xMSlYAyBSy1-E_pouvDq4lp8QjG51cJoa5Gbrt1bg
 tDHinJsLncG1RIsGr_cz1rQw8_GlI_-GdhqFBw8dVjlsgykSf5tfaiiRwORmz7VH_AAk-935a
 VlxMg4mxkbOvN4YDxRLhLb4Y78TA47F__ivNsM4gLD8CHzOUmTEta_pjpZGzsErmYvzDOV6F7
 rOZcRhZThJxLvR3zskrtx83iaNHTwph53bkHNOQzC66wxNMar_T4HMRWzqnrr-sFIcOwLFsWJK
 owc1rQuadjv-ew541YQLaVmkEcai6leZLwCfCTcsxMX9rt0AmOFg'
HTTP/1.1 200 OK                                            
Content-Type: application/json
content-length: 90

{
    "city": "Lyon",
    "deviceId": "a1b2",
    "email": "[email protected]",
    "makePublic": true,
    "username": "foo"
}

We can access the resource, because we have a valid token for user foo.

tip The token value fits on a single line, and there is only a single space between Bearer and the token.

The token is passed with the Authorization HTTP header, and the value is prefixed with Bearer. Here the token allows us to access the resource /api/v1/foo, since the token was generated for user foo. If we try to do the same thing without a token, or if we try to access the resource of another user, as in the following listing, we get denied.

Listing 8.7 Accessing a resource without a matching JWT token

http :4000/api/v1/abc Authorization:'Bearer 
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJkZXZpY2VJZCI6ImExYjIiLCJpYXQiOjE1NjUx Njc0NzUsImV4cCI6MTU2NTc3MjI3NSwiaXNzIjoiMTBrLXN0ZXBzLWFwaSIsInN1YiI6ImZvb
 yJ9.J_tn2BjMNYE6eFSHwSJ9e8DoCEUr_xMSlYAyBSy1-E_pouvDq4lp8QjG51cJoa5Gbrt1b
 gtDHinJsLncG1RIsGr_cz1rQw8_GlI_-GdhqFBw8dVjlsgykSf5tfaiiRwORmz7VH_AAk-935
 aVlxMg4mxkbOvN4YDxRLhLb4Y78TA47F__ivNsM4gLD8CHzOUmTEta_pjpZGzsErmYvzDOV6F
 7rOZcRhZThJxLvR3zskrtx83iaNHTwph53bkHNOQzC66wxNMar_T4HMRWzqnrr-sFIcOwLFsW
 JKowc1rQuadjv-ew541YQLaVmkEcai6leZLwCfCTcsxMX9rt0AmOFg'
HTTP/1.1 403 Forbidden                                        
content-length: 0

We are denied access to a resource of user abc because we passed a (valid) token for user foo.

8.2.2 What is in a JWT token?

So far so good, but what is in the token string?

If you look closely, you will see that a JWT token string is a big line with three parts, each separated by a dot. The three parts are of the form header.payload.signature:

  • header is a JSON document specifying the type of token and the signature algorithm being used.

  • payload is a JSON document containing claims, which are JSON entries where some are part of the specification and some can be free-form.

  • signature is the signature of the header and payload with either a shared secret or a private key, depending on what algorithm you chose.

The header and payload are encoded with the Base64 algorithm. If you decode the JWT token obtained in listing 8.5, the header contains the following:

{
  "typ": "JWT",
  "alg": "RS256"
}

This is what the payload contains:

{
  "deviceId": "a1b2",
  "iat": 1565167475,
  "exp": 1565772275,
  "iss": "10k-steps-api",
  "sub": "foo"
}

Here, deviceId is the device identifier for user foo, sub is the subject (user foo), iat is the date when the token was issued, exp is the token expiration date, and iss is the token issuer (our service).

The signature allows you to check that the content of both the header and payload have been signed by the issuer and have not been modified, as long as you know the public key. This makes JWT tokens a great option for authorization and access control in APIs; a token with all needed claims is self-contained and does not require you to make checks for each request against an identity management service like an LDAP/ OAuth server.

It is important to understand that anyone with a JWT token can decode its content, because Base64 is not an encryption algorithm. You must never put sensitive data like passwords in tokens, even if they are transmitted over secure channels like HTTPS. It is also important to set token expiration dates, so that a compromised token cannot be used indefinitely. There are various strategies for dealing with JWT token expiration, like maintaining a list of compromised tokens in the backend, and combining short expiration deadlines with frequent validity extension requests from clients, where the issuer resends the token, but with an extended exp claim.

8.2.3 Handling JWT tokens with Vert.x

The first thing we need in order to issue and check tokens is a pair of public and private RSA keys, so we can sign JWT tokens. You can generate these using the shell script in the following listing.

Listing 8.8 Generating RSA 2048 public and private keys

#!/bin/bash
openssl genrsa -out private.pem 2048
openssl pkcs8 -topk8 -inform PEM -in private.pem -out 
 private_key.pem -nocrypt
openssl rsa -in private.pem -outform PEM -pubout -out public_key.pem

The next listing shows a helper class to read the PEM files as a string.

Listing 8.9 Helper to read RSA keys

class CryptoHelper {

  static String publicKey() throws IOException {
    return read("public_key.pem");
  }

  static String privateKey() throws IOException {
    return read("private_key.pem");
  }

  private static String read(String file) throws IOException {
    Path path = Paths.get("public-api", file);
    if (!path.toFile().exists()) {                                             
      path = Paths.get("..", "public-api", file);
    }
    return String.join("
", Files.readAllLines(path, StandardCharsets.UTF_8));
  }
}

This allows us to run the service from either the service folder or the application project root.

Joins all lines, separating them with a newline character

Note that the code in CryptoHelper uses blocking APIs. Since this code is run once at initialization, and PEM files are small, we can afford a possible yet negligible blocking of the event loop.

We can then create a Vert.x JWT handler as follows.

Listing 8.10 Creating a JWT handler

String publicKey = CryptoHelper.publicKey();
String privateKey = CryptoHelper.privateKey();

jwtAuth = JWTAuth.create(vertx, new JWTAuthOptions()          
  .addPubSecKey(new PubSecKeyOptions()
    .setAlgorithm("RS256")
    .setBuffer(publicKey))
  .addPubSecKey(new PubSecKeyOptions()
    .setAlgorithm("RS256")
    .setBuffer(privateKey)));

JWTAuthHandler jwtHandler = JWTAuthHandler.create(jwtAuth);   

jwtAuth is a private field of type JWTAuth.

Vert.x router handler for JWT authentication

The JWT handler can be used for routes that require JWT authentication, as it decodes the Authorization header to extract JWT data.

The following listing recalls a route with the handler in its handlers chain.

Listing 8.11 JWT handler in a route

router.get(prefix + "/:username/:year/:month")
  .handler(jwtHandler)                           
  .handler(this::checkUser)
  .handler(this::monthlySteps);

The JWT handler

The JWT handler supports the common authentication API from the vertx-auth-common module, which offers a unified view across different types of authentication mechanisms like databases, OAuth, or Apache .htdigest files. The handler puts authentication data in the routing context.

The following listing shows the implementation of the checkUser method where we check that the user in the JWT token is the same as the one in the HTTP request path.

Listing 8.12 Checking that a valid JWT token is present

private void checkUser(RoutingContext ctx) {
  String subject = ctx.user().principal().getString("sub");    
  if (!ctx.pathParam("username").equals(subject)) {            
    sendStatusCode(ctx, 403);
  } else {
    ctx.next();                                                
  }
}

User name from the JWT token

User name specified in the HTTP request path

Pass to the next handler

This provides a simple separation of concerns, as the checkUser handler focuses on access control and delegates to the next handler in the chain by calling next if access is granted, or ends the request with a 403 status code if the wrong user is trying to access a resource.

Knowing that access control is correct, the monthlySteps method in the following listing can focus on making the request to the activity service.

Listing 8.13 Getting monthly steps data

private void monthlySteps(RoutingContext ctx) {
  String deviceId = ctx.user().principal().getString("deviceId");     
  String year = ctx.pathParam("year");
  String month = ctx.pathParam("month");
  webClient
    .get(3001, "localhost", "/" + deviceId + "/" + year + "/" + month)
    .as(BodyCodec.jsonObject())
    .rxSend()
    .subscribe(
      resp -> forwardJsonOrStatusCode(ctx, resp),
      err -> sendBadGateway(ctx, err));
}

From the JWT token

The device identifier is extracted from the JWT token data and passed along to the web client request.

8.2.4 Issuing JWT tokens with Vert.x

Last, but not least, we need to generate JWT tokens. To do that, we need to make two requests to the user profile service: first we need to check the credentials, and then we gather profile data to prepare a token.

The following listing shows the handler for the /api/v1/token route.

Listing 8.14 JWT token-creation router handler

private void token(RoutingContext ctx) {
  JsonObject payload = ctx.getBodyAsJson();                
  String username = payload.getString("username");
  webClient
    .post(3000, "localhost", "/authenticate")              
    .expect(ResponsePredicate.SC_SUCCESS)
    .rxSendJson(payload)
    .flatMap(resp -> fetchUserDetails(username))           
    .map(resp -> resp.body().getString("deviceId"))
    .map(deviceId -> makeJwtToken(username, deviceId))     
    .subscribe(
      token -> sendToken(ctx, token),
      err -> handleAuthError(ctx, err));
}

private void sendToken(RoutingContext ctx, String token) {
  ctx.response().putHeader("Content-Type", "application/jwt").end(token);
}

private void handleAuthError(RoutingContext ctx, Throwable err) {
  logger.error("Authentication error", err);
  ctx.fail(401);
}

We extract the credentials from the request to /api/v1/token.

We first issue an authentication request.

On success, we make another request to get the profile data.

We prepare the token.

This is a typical RxJava composition of asynchronous operations with flatMap to chain requests. You can also see the declarative API of the Vert.x router, where we can specify that we expect the first request to be a success.

The following listing shows the implementation of fetchUserDetails, which gets the user profile data after the authentication request has succeeded.

Listing 8.15 Fetching user details

private Single<HttpResponse<JsonObject>> fetchUserDetails(String username) {
  return webClient
    .get(3000, "localhost", "/" + username)
    .expect(ResponsePredicate.SC_OK)         
    .as(BodyCodec.jsonObject())
    .rxSend();
}

We expect a success.

Finally, the next listing shows how to prepare a JWT token.

Listing 8.16 Preparing a JWT token

private String makeJwtToken(String username, String deviceId) {
  JsonObject claims = new JsonObject()                          
    .put("deviceId", deviceId);
  JWTOptions jwtOptions = new JWTOptions()
    .setAlgorithm("RS256")
    .setExpiresInMinutes(10_080) // 7 days
    .setIssuer("10k-steps-api")                                 
    .setSubject(username);
  return jwtAuth.generateToken(claims, jwtOptions);
}

Our custom claims

A claim that is in the JWT specification

The JWTOptions class offers methods for the common claims from the JWT RFC, such as the issuer, expiration date, and subject. You can see that we did not specify when the token was issued, although there is a method for that in JWTOptions. The jwtAuth object does the right thing here and adds it on our behalf.

8.3 Cross-origin resource sharing (CORS)

We have a public API that forwards requests to internal services, and this API uses JWT tokens for authentication and access control. I also demonstrated on the command line that we can interact with the API. In fact, any third-party application can talk to our API over HTTP: a mobile phone application, another service, a desktop application, and so on. You might think that web applications could also talk to the API from JavaScript code running in web browsers, but it is (fortunately!) not that simple.

8.3.1 What is the problem?

Web browsers enforce security policies, and among them is the same-origin policy. Suppose we load app.js from https://my.tld:4000/js/app.js:

  • app.js is allowed to make requests to https://my.tld:4000/api/foo/bar.

  • app.js is not allowed to make requests to https://my.tld:4001/a/b/c because a different port is not the same origin.

  • app.js is not allowed to make requests to https://other.tld/123 because a different host is not the same origin.

Cross-origin resource sharing (CORS) is a mechanism by which a service can allow incoming requests from other origins (https://fetch.spec.whatwg.org/). For instance, the service exposing https://other.tld/123 can specify that cross-origin requests are allowed from code served from https://my.tld:4000, or even from any origin. This allows web browsers to proceed with a cross-origin request when the request origin allows it; otherwise it will deny it, which is the default behavior.

When a cross-origin request is triggered, such as to load some JSON data, an image, or a web font, the web browser sends a request to the server with the requested resource, and passes an Origin HTTP header. The server then responds with an Access-Control-Allow-Origin HTTP header with the allowed origin, as illustrated in figure 8.4.

Figure 8.4 Example CORS interaction

A value of "*" means that any origin can access the resource, whereas a value like https://my.tld means that only cross-origin requests from https://my.tld are allowed. In figure 8.4, the request succeeds with the JSON payload, but if the CORS policy forbids the call, the app.js code would get an error while attempting to make a cross-origin request.

Depending on the type of cross-origin HTTP request, web browsers do simple or preflighted requests. The request in figure 8.4 is a simple one. By contrast, a PUT request would need a preflighted request, as it can potentially have side effects (PUT implies modifying a resource), so a preflight OPTIONS HTTP request to the resource must be made to check what the CORS policy is, followed by the actual PUT request when allowed. Preflighted requests provide more detail, such as the allowed HTTP headers and methods, because a server can, for example, have a CORS policy of forbidding doing DELETE requests or having an ABC header in the HTTP request. I recommend reading Mozilla’s “Cross-Origin Resource Sharing (CORS)” document (http://mng .bz/X0Z6), as it provides a detailed and approachable explanation of the interactions between browsers and servers with CORS.

8.3.2 Supporting CORS with Vert.x

Vert.x comes with a ready-to-use CORS handler with the CorsHandler class. Creating a CorsHandler instance requires three settings:

  • The allowed origin pattern

  • The allowed HTTP headers

  • The allowed HTTP methods

The following listing shows how to install CORS support in a Vert.x router.

Listing 8.17 Installing CORS support in a router

Set<String> allowedHeaders = new HashSet<>();        
allowedHeaders.add("x-requested-with");
allowedHeaders.add("Access-Control-Allow-Origin");
allowedHeaders.add("origin");
allowedHeaders.add("Content-Type");
allowedHeaders.add("accept");
allowedHeaders.add("Authorization");

Set<HttpMethod> allowedMethods = new HashSet<>();    
allowedMethods.add(HttpMethod.GET);
allowedMethods.add(HttpMethod.POST);
allowedMethods.add(HttpMethod.OPTIONS);
allowedMethods.add(HttpMethod.PUT);

router.route().handler(CorsHandler                   
  .create("*")
  .allowedHeaders(allowedHeaders)
  .allowedMethods(allowedMethods));

The set of allowed HTTP headers

The set of allowed HTTP methods

A CORS handler for all origins

The HTTP methods are those supported in our API. You can see that we don’t support DELETE, for instance. The CORS handler has been installed for all routes, since they are all part of the API and should be accessible from any kind of application, including web browsers. The allowed headers should match what your API needs, and also what clients may pass, like specifying a content type, or headers that could be injected by proxies and for distributed tracing purposes.

We can check that CORS is properly supported by making an HTTP OPTIONS preflight request to one of the routes supported by the API.

Listing 8.18 Checking CORS support

$ http OPTIONS :4000/api/v1/token Origin:'http://foo.tld'
HTTP/1.1 405 Method Not Allowed
access-control-allow-origin: *
content-length: 0

By specifying an origin HTTP header, the CORS handler inserts an access-control-allow-origin HTTP header in the response. The HTTP status code is 405, since the OPTION HTTP method is not supported by the specific route, but this is not an issue as web browsers are only interested in the CORS-related headers when they do a preflight request.

8.4 A modern web frontend

We have discussed the interesting points in the public API: how to make HTTP requests with the Vert.x web client, how to use JWT tokens, and how to enable CORS support. It is now time to see how we can expose the user web application (defined in chapter 7), and how that application can connect to the public API.

The application is written with the Vue.js JavaScript framework. Vert.x is used to serve the application’s compiled assets: HTML, CSS, and JavaScript.

The corresponding source code is located in the part2-steps-challenge/user-webapp folder of the book’s source code repository.

8.4.1 Vue.js

Vue.js deserves a book by itself, and we recommend that you read Erik Hanchett and Benjamin Listwon’s Vue.js in Action (Manning, 2018) if you are interested in learning this framework. I’ll provide a quick overview here, since we’re using Vue.js as the JavaScript framework for the two web applications developed as part of the larger 10k steps application.

Vue.js is a modern JavaScript frontend framework, like React or Angular, for building modern web applications, including single-page applications. It is reactive as changes in a component model trigger changes in the user interface. Suppose that we display a temperature in a web page. When the corresponding data changes, the temperature is updated, and Vue.js takes care of (most) of the plumbing for doing that.

Vue.js supports components, where an HTML template, CSS styling, and JavaScript code can be grouped together, as in the following listing.

Listing 8.19 Canvas of a Vue.js component

<template>
  <div id="app">
    {{ hello }}                     
  </div>
</template>

<style scoped>                      
  div {
    border: solid 1px black;
  }
</style>

<script>
  export default {
    data() {
      return {
        hello: "Hello, world!"      
      }
    }
  }
</script>

Replaced by the value of the hello property

CSS rules local to the component

The initial definition of the hello property

A Vue.js project can be created using the Vue.js command-line interface (https://cli .vuejs.org/):

$ vue create user-webapp

The yarn build tool can then be used to install dependencies (yarn install), serve the project for development with automatic live-reload (yarn run serve), and build a production version of the project HTML, CSS, and JavaScript assets (yarn run build).

8.4.2 Vue.js application structure and build integration

The user web application is a single-page application with three different screens: a login form, a page with user details, and a registration form.

The key Vue.js files are the following:

  • src/main.js--The entry point

  • src/router.js--The Vue.js router that dispatches to the components of the three different screens

  • src/DataStore.js--An object to hold the application store using the web browser local storage API, shared among all screens

  • src/App.vue--The main component that mounts the Vue.js router

  • src/views--Contains the three screen components: Home.vue, Login.vue, and Register.vue

The Vue.js router configuration is shown in the following listing.

Listing 8.20 Vue.js router configuration

import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import Login from './views/Login.vue'
import Register from './views/Register.vue'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/',        
      name: 'home',     
      component: Home   
    },
    {
      path: '/login',
      name: 'login',
      component: Login
    },
    {
      path: '/register',
      name: 'register',
      component: Register
    },
  ]
})

Component path

Component name

Component reference

The application code is colocated in the same module as the Vert.x application that serves the user web application, so you will find the usual Java source files under src/main/java and a Gradle build.gradle.kts file. The Vue.js compiled assets (yarn build) must be copied to src/main/resources/webroot/assets for the Vert.x-based service to serve them.

This makes for two build tools in a single project, and fortunately they can coexist peacefully. In fact, it is very easy to call yarn from Gradle, as the com.moowork.node Gradle plugin provides a self-contained Node environment. The following listing shows the Node-related configuration of the user web application Gradle build file.

Listing 8.21 Using the com.moowork.node Gradle plugin

import com.moowork.gradle.node.yarn.YarnTask
apply(plugin = "com.moowork.node")                             
tasks.register<YarnTask>("buildVueApp") {                      
  dependsOn("yarn_install")                                    
  // (...)                                                     
  args = listOf("build")                                       
}
tasks.register<Copy>("copyVueDist") {                          
  dependsOn("buildVueApp")
  from("$projectDir/dist")
  into("$projectDir/src/main/resources/webroot/assets")
}
val processResources by tasks.named("processResources") {      
  dependsOn("copyVueDist")
}
val clean by tasks.named<Delete>("clean") {                    
  delete("$projectDir/dist")
  delete("$projectDir/src/main/resources/webroot/assets")
}
// (...)

Uses the Node plugin

Creates a task to call yarn

Adds a dependency on running yarn install first

Gradle caching instructions that you can find in the full source code

Calls yarn build

Task to copy the compiled assets

Make sure building the project also builds the Vue.js application.

Extra clean task to be done for the Vue.js compiled assets

The buildVueApp and copyVueDist tasks are inserted as part of the regular project build tasks, so the project builds both the Java Vert.x code and the Vue.js code. We also customize the clean task to remove the generated assets.

8.4.3 Backend integration illustrated

Let’s look at one of the Vue.js components: the login screen shown in figure 8.5.

Figure 8.5 Screenshot of the login screen

The file for this component is src/views/Login.vue. The component shows the login form, and when submitted it must call the public API to get a JWT token. On success, it must store the JWT token locally and then switch the view to the home component. On error, it must stay on the login form and display an error message.

The HTML template part of the component is shown in the following listing.

Listing 8.22 Login component HTML template

<template>
  <div>
    <div class="alert alert-danger" role="alert" 
     v-if="alertMessage.length > 0">                                   
      {{ alertMessage }}                                                 
    </div>
    <form v-on:submit="login">                                           
      <div class="form-group">
        <label for="username">User name</label>
        <input type="username" class="form-control" id="username" 
 placeholder="somebody123" v-model="username">                
      </div>
      <div class="form-group">
        <label for="password">Password</label>
        <input type="password" class="form-control" id="password" placeholder="abc123" v-model="password">
      </div>
      <button type="submit" class="btn btn-primary">Submit</button>
    </form>
    <div>
      <p>...or <router-link to="/register">register</router-link></p>    
    </div>
  </div>
</template>

Conditionally display the div block depending on the value of the alertMessage component data.

Template syntax to render the value of alertMessage

Call the login method on form submit.

v-model binds the field value to the username component data.

<router-link> allows linking to another component.

The JavaScript part of the component provides the component data declaration as well as the login method implementation. We use the Axios JavaScript library to make HTTP client calls to the public API. The following listing provides the component JavaScript code.

Listing 8.23 Login component JavaScript code

import DataStore from '../DataStore'
import axios from 'axios'

export default {
  data() {                                                             
    return {
      username: '',
      password: '',
      alertMessage: ''
    }
  },
  methods: {                                                           
    login: function () {
      if (this.username.length === 0 || this.password.length === 0) {  
        return
      }
      axios
        .post("http://localhost:4000/api/v1/token", {                  
          username: this.username,
          password: this.password
        })
        .then(response => {
          DataStore.setToken(response.data)                            
          DataStore.setUsername(this.username)
          this.$router.push({name: 'home'})                            
        })
        .catch(err => this.alertMessage = err.message)                 
    }
  }
}

Component data declaration

Component methods declaration

If either of the fields is empty, there is no point in trying to authenticate against the public API.

Issue an authentication request with the credentials as a JSON payload.

In case of success, store the token and username from the response.

Tell the router to change component.

Triggers the error message to be reactively displayed when the value of alertMessage changes

The component data properties are updated as the user types text in the username and password fields, and the login method is called on form submit. If the call succeeds, the application moves to the home component.

The next listing is from the code of the Home.vue component, and it shows how you can use the JWT token to fetch the user’s total number of steps.

Listing 8.24 Using the JWT token with Axios

axios
  .get(`http://localhost:4000/api/v1/${DataStore.username()}/total`, {
    headers: {
      'Authorization': `Bearer ${DataStore.token()}`          
    }
  })
  .then(response => this.totalSteps = response.data.count)    
  .catch(err => {
    if (err.response.status === 404) {
      this.totalSteps = 0
    } else {
      this.alertMessage = err.message
    }
  })

Pass the token from the value fetched by the login component.

Update the component data, triggering a view refresh.

Let’s now see how we can serve the web application assets with Vert.x.

8.4.4 Static content serving with Vert.x

The Vert.x code does not have much to do beyond starting an HTTP server and serving static content. The following listing shows the content of the rxStart method of the UserWebAppVerticle class.

Listing 8.25 Serving static content with Vert.x

@Override
public Completable rxStart() {
  Router router = Router.router(vertx);

  router.route().handler(StaticHandler.create("webroot/assets"));    
  router.get("/*").handler(ctx -> ctx.reroute("/index.html"));       

  return vertx.createHttpServer()
    .requestHandler(router)
    .rxListen(HTTP_PORT)
    .ignoreElement();
}

Resolve static content against webroot/assets in the classpath.

Alias /* to /index.html.

The StaticHandler caches files in memory, unless configured otherwise in the call to the create method. Disabling caching is useful in development mode, because you can modify static assets’ content and see changes by reloading in a web browser without having to restart the Vert.x server. By default, static files are resolved from the webroot folder in the classpath, but you can override it as we did by specifying webroot/assets.

Now that we’ve discussed how to use the Vert.x web stack, it is time to focus on testing the services that compose the reactive application.

8.5 Writing integration tests

Testing is a very important concern, especially as there are multiple services involved in the making of the 10k steps challenge reactive application. There is no point in testing that the user web application service delivers static content properly, but it is crucial to have tests covering interactions with the public API service. Let’s discuss how to write integration tests for this service.

The public API source code reveals an IntegrationTest class. It contains several ordered test methods that check the API behavior:

  1. Register some users.

  2. Get a JWT token for each user.

  3. Fetch a user’s data.

  4. Try to fetch the data of another user.

  5. Update a user’s data.

  6. Check some activity stats for a user.

  7. Try to check the activity of another user.

Since the public API service depends on the activity and user profile services, we either need to mock them with fake services that we run during the tests’ execution, or deploy them along with all their dependencies, like databases. Either approach is fine. In the chapters in this part we will sometimes create a fake service for running our integration tests, and sometimes we will just deploy the real services.

In this case, we are going to deploy the real services, and we need to make this from JUnit 5 in a self-contained and reproducible manner. We first need to add the project dependencies, as in the following listing.

Listing 8.26 Test dependencies to run the integration tests

testImplementation(project(":user-profile-service"))                         
testImplementation(project(":activity-service"))
testImplementation("io.vertx:vertx-pg-client:$vertxVersion")                 
testImplementation("org.testcontainers:junit-jupiter:$testContainersVersion")

testImplementation("org.junit.jupiter:junit-jupiter-api:$junit5Version")
testImplementation("io.vertx:vertx-junit5:$vertxVersion")
testImplementation("io.vertx:vertx-junit5-rx-java2:$vertxVersion")
testImplementation("io.rest-assured:rest-assured:$restAssuredVersion")       
testImplementation("org.assertj:assertj-core:$assertjVersion")

Dependency on another project module

This is used to insert data in PostgreSQL. More on that later.

This is to run Docker containers.

A nice DSL library for testing HTTP services

These dependencies bring us two useful tools for writing tests:

  • Testcontainers is a project for running Docker containers in JUnit tests, so we will be able to use infrastructure services like PostgreSQL or Kafka (www.test containers.org).

  • REST Assured is a library focusing on testing HTTP services, providing a convenient fluent API for describing requests and response assertions (http://rest-assured.io).

The preamble of the test class is shown in the following listing.

Listing 8.27 Preamble of the integration test class

@ExtendWith(VertxExtension.class)                                    
@TestMethodOrder(OrderAnnotation.class)                              
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@DisplayName("Integration tests for the public API")
@Testcontainers                                                      
class IntegrationTest {

  @Container
  private static final DockerComposeContainer CONTAINERS =
    new DockerComposeContainer(new File("../docker-compose.yml"));   
  // (...)
}

Use the Vert.x JUnit 5 support.

Test methods must be run in order.

Use Testcontainers support.

Start containers from a Docker Compose file.

Testcontainers gives lots of choices for starting one or many containers. It supports generic Docker images, specialized classes for common infrastructure (PostgreSQL, Apache Kafka, etc.), and Docker Compose. Here we reuse the Docker Compose descriptor for running the whole application (docker-compose.yml), and the containers described in the file are started before the first test is run. The containers are destroyed when all tests have executed. This is very interesting--we get to write integration tests against the real infrastructure services that would be used in production.

The prepareSpec method is annotated with @BeforeAll and is used to prepare the tests. It inserts some data in the PostgreSQL database for the activity service and then deploys the user profile and activity verticles. It also prepares a RequestSpecification object from REST Assured, as follows.

Listing 8.28 Preparing a REST Assured request specification

requestSpecification = new RequestSpecBuilder()
  .addFilters(asList(new ResponseLoggingFilter(), new RequestLoggingFilter()))
  .setBaseUri("http://localhost:4000/")
  .setBasePath("/api/v1")                                                     
  .build();

All requests and responses will be logged, which is useful for tracking errors.

This avoids repeating the base path of all URLs in requests.

This object is shared among all tests methods, as they all have to make requests to the API. We enable logging of all requests and responses for easier debugging, and we set /api/v1 as the base path for all requests.

The test class maintains a hash map of users to register and later use in calls, as well as a hash map of JWT tokens.

Listing 8.29 Utility hash maps for the integration test

private final HashMap<String, JsonObject> registrations = new 
 HashMap<String, JsonObject>() {                               
  {
    put("Foo", new JsonObject()
      .put("username", "Foo")
      .put("password", "foo-123")
      .put("email", "[email protected]")
      .put("city", "Lyon")
      .put("deviceId", "a1b2c3")
      .put("makePublic", true));
    // (...)
};

private final HashMap<String, String> tokens = new HashMap<>();  

Users

JWT tokens, once retrieved

The following listing is the first test, where the users from the registrations hash map are registered.

Listing 8.30 Test for registering users

@Test
@Order(1)
@DisplayName("Register some users")
void registerUsers() {
  registrations.forEach((key, registration) -> {
    given(requestSpecification)
      .contentType(ContentType.JSON)
      .body(registration.encode())     
      .post("/register")               
      .then()
      .assertThat()
      .statusCode(200);                
  });
}

We encode the JSON data to a string.

HTTP POST to /api/v1/register

Assert that the status code is a 200.

The REST Assured fluent API allows us to express our request and then do an assertion on the response. It is possible to extract a response as text or JSON to perform further assertions, as in the next listing, which is extracted from the test method that retrieves JWT tokens.

Listing 8.31 Test code for retrieving JWT tokens

JsonObject login = new JsonObject()
  .put("username", key)
  .put("password", registration.getString("password"));

String token = given(requestSpecification)
  .contentType(ContentType.JSON)
  .body(login.encode())
  .post("/token")
  .then()
  .assertThat()
  .statusCode(200)
  .contentType("application/jwt")    
  .extract()                         
  .asString();
assertThat(token)                    
  .isNotNull()
  .isNotBlank();

tokens.put(key, token);

Assert that the content-type header is in the response and matches that of JWT tokens.

Extract the response.

AssertJ assertions on a String

The test fetches a token and then asserts that the token is neither a null value or a blank string (empty or with spaces). Extracting JSON data is similar, as shown next.

Listing 8.32 Extracting JSON with REST Assured

JsonPath jsonPath = given(requestSpecification)
  .headers("Authorization", "Bearer " + tokens.get("Foo"))         
  .get("/Foo/total")
  .then()
  .assertThat()
  .statusCode(200)
  .contentType(ContentType.JSON)
  .extract()
  .jsonPath();

assertThat(jsonPath.getInt("count")).isNotNull().isEqualTo(6255);  

Pass a JWT token.

Work with a JSON representation.

The test fetches the total number of steps for user Foo, extracts the JSON response, and then checks that the step count (the count key in the JSON response) is equal to 6255.

The integration test can be run with Gradle (./gradlew :public-api:test) or from a development environment, as shown in figure 8.6.

Figure 8.6 Running the integration tests from IntelliJ IDEA

You now have a good understanding of using the Vert.x web stack both for exposing endpoints and consuming other services. The next chapter focuses on the messaging and event streaming stack of Vert.x.

Summary

  • The Vert.x web module makes it easy to build an edge service with CORS support and HTTP calls to other services.

  • JSON web tokens are useful for authorization and access control in a public API.

  • Vert.x does not have a preference regarding frontend application frameworks, but it is easy to integrate a Vue.js frontend application.

  • By combining Docker containers managed from Testcontainers and the Rest Assured library, you can write integration tests for HTTP APIs.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.15.149