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
- 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.
- Basic knowledge of Vertx.
- Knowledge of HTTP methods and how to perform calls using Postman or a similar API client.
What is a resource server?
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:
- The resource server hosts information (resources) that should be protected
- 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
<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>
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;
}
}
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");
}
});
}
}
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");
});
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-auth-jwt</artifactId>
<version>3.9.3</version>
</dependency>
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();
}
}
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:
- We intercept all the calls performed to /api/* (or anything beneath it).
- We pass the call to the handler validateTokens(), which ensures that the request contains a valid JWT.
- If the token is not valid (or is not present), we return HTTP 401 Unauthorized and an empty body response.
- 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
Post a Comment