Enabling Vertx Web as a Resource Server

Enabling Vertx as a Resource Server. Source: Author.

The evolution of the web has demanded the creation of more robust and sophisticated ways of securing our apps. With that purpose, protocols such as OAuth 2.0 and OIDC 1.0 have been created. Correctly implemented, these protocols can ensure not only that our APIs are secured, but they also ensure a safe exchange of data with third party applications.

In this tutorial we'll enable Vertx - Eclipse's framework for reactive apps - as a Resource server. We'll secure our endpoints and ensure that only clients with a valid JWT can access our data.

Pre-requirements

  1. Knowledge of the actors involved in an OAuth process. These actors are: the authorization server, the resource server, the client app and the resource owner.
  2. Basic knowledge of Vertx.
  3. Knowledge of HTTP methods and how to perform calls using Postman or a similar API client.
In case you are still not familiar or haven't completely understood the concepts of OAuth, I can recommend you this guide by our friends from Okta. Let's start!

Estimated required time: 15min

What is a resource server?

Since we're enabling our Vertx server as a resource server, it would be convenient to clearly identify what a resource server is. According to the RFC 6749 - The OAuth 2.0 Authorization Framework, a resource server is defined as:
The server hosting the protected resources, capable of accepting and responding to protected resource requests using access tokens.

That concept leaves us with two important premises: 

  1. The resource server hosts information (resources) that should be protected
  2. To access this information, requests should contain a valid access token

That leads us to a second question. What is an access token? 

Access tokens are credentials used to access protected resources.  An access token is a string representing an authorization issued to the client. [...] Access tokens can have different formats, structures, and methods of utilization (e.g., cryptographic properties) based on the resource server security requirements.

This flexibility of the access tokens, allows us to use JWT tokens as access tokens:

JWTs can be used as OAuth 2.0 Bearer Tokens to encode all relevant parts of an access token into the access token itself instead of having to store them in a database. - OAuth.net

The app we're about to develop complies with both of these premises: First, our resource server will contain a couple of endpoints that will be protected. Second, we'll require a valid JWT to access the information protected in these APIs. 

 

Developing the App

1. Setting up the Vertx server

The first step is to setup our Vertx server. For that purpose we'll use maven and include the following dependencies: 
<dependencies>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<version>3.9.3</version>
</dependency>

<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
<version>3.9.3</version>
</dependency>
</dependencies>
vertx-core is the main artifact that allows us to create vertx apps. On the other hand, vertx-web enables the creation of APIs with the use of an embedded web server.

Next, we're going to create a Verticle. The Verticle will override the method start() from AbstractVerticle. In this overriden method we're going to create our Http server and set it to listen to the port 8080.
In order to get the server working, we also need a router. As for now, we'll configure a single endpoint to indicate that our server is up, running and listening to the designated port.

VertxHttpVerticle.java
public class VertxHttpVerticle extends AbstractVerticle {
@Override
public void start() {
Router router = configureRouter();
vertx.createHttpServer().requestHandler(router).listen(8080);
}

private Router configureRouter() {
Router router = Router.router(vertx);

router.route("/").handler(routingContext -> {
routingContext.response().putHeader(HttpHeaders.CONTENT_TYPE, "text/html")
.end("<h1>The Vertx server is running!</h1>");
});

return router;
}
}

The last step in setting up our application is to deploy the verticle we just created. For that we'll create a new class and add the main method to this class.

public class VertxHttpAPI {

public static void main(String[] args) {
Vertx vertx = Vertx.vertx();
vertx.deployVerticle(new VertxHttpVerticle(), res -> {
if (res.succeeded()) {
System.out.println("HTTP server listening on port 8080");
}
});
}
}
Now when we run the app, we'll get the message "HTTP server listening on port 8080" in the console. We can access the endpoint / in our server by pasting the address localhost:8080/ in our browser.




2. Securing our endpoints
So far we have an app with a single endpoint running on the port 8080. We'll add a couple of endpoints that we'll secure through token authentication. For this tutorial we'll add 3 endpoints: /api/users, /api/secret, /api/admin

router.route("/api/users").handler(routingContext -> {
routingContext.response().setStatusCode(200);
routingContext.response().end("Here goes a secret list of users");
});

router.route("/api/secret").handler(routingContext -> {
routingContext.response().setStatusCode(200);
routingContext.response().end("This is a super SECRET endpoint");
});

router.route("/api/admin").handler(routingContext -> {
routingContext.response().setStatusCode(200);
routingContext.response().end("Only users with a valid token have access to this endpoint");
});

These 3 endpoints are added in the method configureRouter(). If we re-run the app, we can access these APIs from our browser.




We've verified that our APIs are working. Now we need to secure them. In order to do so, we'll import a new library: vertx-auth-jwt. Add the following dependency to your pom.xml

<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-auth-jwt</artifactId>
<version>3.9.3</version>
</dependency>

We'll secure everything beneath /api/, therefore we need a new handler for /api/* This handler will be the responsible for verifying the validity of the json web tokens and deciding whether a request should have access to the API or not. 
For this purpose, vertx-auth-jwt provides a mechanism to authenticate JWTs based on a public key. So all we need to know is the public key from our authorization server and configure it in our handler as follows:
private void validateTokens(RoutingContext routingContext){
JWTAuth provider = JWTAuth.create(vertx, new JWTAuthOptions()
.addPubSecKey(new PubSecKeyOptions(new JsonObject().put("type", "RS256").put("publicKey", "YOUR_PUBLIC_KEY_GOES_HERE"))));
String jwt = routingContext.request().getHeader("Authorization");
if (jwt != null && !jwt.equals("")) {
jwt = jwt.substring(7);
provider.authenticate(new JsonObject().put("jwt", jwt),
result -> {
if (result.succeeded()) {
routingContext.next();
} else {
routingContext.response().setStatusCode(401);
routingContext.response().end();
}
});
} else {
routingContext.response().setStatusCode(401);
routingContext.response().end();
}
}
Make sure to substitute the string "YOUR_PUBLIC_KEY_GOES_HERE" with the public key used by your authorization server.

The last step in securing the APIs is to catch all requests to /api/*. For this we add a new handler before the individual apis. The reason for this is that:
A router takes an HTTP request and finds the first matching route for that request, and passes the request to that route. The route can have a handler associated with it, which then receives the request. You then do something with the request, and then, either end it or pass it to the next matching handler. - Vertx Web Documentation

So we add the following line to the method configureRouter()

router.route("/api/*").handler(this::validateTokens); 

The logic here is:

  1. We intercept all the calls performed to /api/* (or anything beneath it).
  2. We pass the call to the handler validateTokens(), which ensures that the request contains a valid JWT.
  3. If the token is not valid (or is not present), we return HTTP 401 Unauthorized and an empty body response.
  4. If the token is valid, then we tell the routing context to continue and look for the next route that matches the URL. Once this is found, a new handler takes charge, executes the pertinent tasks and sends the response back to the user.

In this sense, the handler validateTokens() works as some sort of security proxy for the rest of our APIs. 


Our APIs are secure now. It's time to test.

If we try to access any of the endpoints under api/* via the web browser, we'll see the following page:


However, unprotected endpoints will still be available for public consumption of data:


Now, we'll test using Postman. We'll perform three tests: The first one will try to access a protected endpoint without a JWT, the second with an invalid JWT and a third one with a valid JWT. You can get a valid JWT from your authorization server. 

Test 1: No token

The result of the first test is similar to that of accessing the API with the browser. We see the response 401 Unauthorized and an empty body response, which is the expected result for this test.


Test 2: Invalid token

In the second test we add an invalid Bearer token as part of our authentication. The result is the same as with test 1.


Test 3: Valid JWT

Our last test involves the correct retrieval of information from the API using a valid JWT. 

As we can see in the image above, this call ends in a response Http 200 and the content "Only users with a valid token have access to this endpoint"; which is exactly the response we were expecting. 

Our APIs are now secure and our Vertx server has been enabled as a resource server. 


In the next article we'll be exploring the possibility of dynamically retrieving the public key from the authorization server. Don't forget to download the source code from Github and leave your feedback in the comments.

See you next week!

Comments