Enabling Vertx Web as a Resource Server - Part II

Enabling Vertx as a Resource Server - Part II. Source: Author.

As we discussed in our previous article, security is a substantial part of the development of web applications. We do not want sensitive information to be disclosed to unauthorized parties. Hence, we devote our efforts to securing our APIs and ensuring that only authorized users have access to the information. 

In the first part of this tutorial, we learned how to enable Vertx Web as a resource server. We identified the need to protect our resources and created a mechanism to enforce every client to send a JWT along with every request to our server. This mechanism works like a charm, but there are still parts of our code that could be better, namely: it is not a good practice to have the public key hardcoded in our app. 

A more suitable alternative would be to have the public key in a separate configuration file and read it from there. This file can be encrypted to make our app safer. There's also a third more-elegant alternative: to consume the public key dynamically from the authorization server. 

In this article, we're gonna learn how to get the public key from our authorization server dynamically. Let's start.

Pre-requirements

2. Basic knowledge of Keycloak or any other OAuth / OIDC provider.
Estimated required time: 15min


Introducing the Authorization Server

We've long talked about the authorization server: that entity in charge of validating users' identity and generating tokens; however, we haven't come in contact with it - until now.
For the present tutorial, we're going to use Keycloak:
Keycloak is an open source Identity and Access Management solution aimed at modern applications and services. It makes it easy to secure applications and services with little to no code. - Keycloak.org
Keycloak serves as an Identity Broker and works with different protocols, such as OIDC and OAuth 2.0. That makes it the perfect fit for our tutorial. 

First, we'll need to setup an instance of Keycloak and configure an user and a realm. Alternatively, you could use an embedded Keycloak instance from our friends from Baeldung. For the sake of simplicity, we're going to use the pre-configured version from Baeldung. 

The realm comes with a couple of interesting URIs worth mentioning:
  1. Realm URI:  
    • Content: It's the base URL of the realm. We could actually get the public key from this URI, but - in my experience- it is better to get it from the dedicated JWKS URI.
    • Example: http://localhost:8083/auth/realms/baeldung/
  2. Well-known URI
    • Content: As part of the OIDC Discovery specification, the well-known URI offers information about the configuration of the OIDC provider. This includes information about other endpoints, such as: authorization, token generation, token introspection, jwks, among others.
    • Example: http://localhost:8083/auth/realms/baeldung/.well-known/openid-configuration
  3. JWKS URI
    • Content: The JSON Web Key Set (JWKS) is a set of keys containing the public keys used to verify any JSON Web Token (JWT) issued by the authorization server and signed using the RS256 signing algorithm.
    • Example: http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/certs

Realm URL

 Well-known URL
 
JWKS URI

Following the definition, the JWKS URI is the one used to verify JWTs. This is the URI we need for our app.

Developing the App

So far we have a resource server that verifies JSON web tokens on the basis of a hardcoded public key - not a safe approach. Therefore, we'll start by deleting that public key and adding the method getKey() to retrieve the key dynamically from the auth server.
To do so, we'll make use of the auth0 library: jwks-rsa:
<dependency>
<groupId>com.auth0</groupId>
<artifactId>jwks-rsa</artifactId>
<version>0.14.0</version>
</dependency>

Then add our method getKey() to our class VertxHttpVerticle:
private String getKey() {
try {
JwkProvider provider = new UrlJwkProvider(new URL(JWKS_URL));
Jwk jwk = provider.get(JWKS_KID);
return Base64.getEncoder().encodeToString(jwk.getPublicKey().getEncoded());
} catch (Exception e) {
return "";
}
}

From the method above, we can see that we need to configure two constants: JWKS_URI and JWKS_KID. The former can be retrieved from the well-known URL in our auth server (URL 3 from the previous section). The latter is present as one of the properties inside the JWKS_URI. We define both constants at the beginning of the file:
private final String JWKS_URL = "http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/certs";
private final String JWKS_KID = "_b78X30O343js3QZcvCJSSHa4zUKPmIBchQmHcNpBUM";
Our class should look as follows:

VertxHttpVerticle.java
public class VertxHttpVerticle extends AbstractVerticle {

private final String JWKS_URL = "http://localhost:8083/auth/realms/baeldung/protocol/openid-connect/certs";
private final String JWKS_KID = "_b78X30O343js3QZcvCJSSHa4zUKPmIBchQmHcNpBUM";

@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>");
});

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

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");
});

return router;
}

private void validateTokens(RoutingContext routingContext) {
JWTAuth provider = JWTAuth.create(vertx, new JWTAuthOptions()
.addPubSecKey(new PubSecKeyOptions(new JsonObject().put("algorithm", "RS256").put("publicKey", getKey()))));
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 {
setUnauthorized(routingContext);
}
});
} else {
setUnauthorized(routingContext);
}
}

private String getKey() {
try {
JwkProvider provider = new UrlJwkProvider(new URL(JWKS_URL));
Jwk jwk = provider.get(JWKS_KID);
return Base64.getEncoder().encodeToString(jwk.getPublicKey().getEncoded());
} catch (Exception e) {
return "";
}
}

private void setUnauthorized(RoutingContext routingContext) {
routingContext.response().setStatusCode(401);
routingContext.response().end();
}
}
A pretty small configuration for a significant improvement in our app. 

Generating tokens in our Auth server

We can easily generate access tokens thanks to Postman. Follow these steps:

1. In the request view, head to the Auth tab and in the dropdown Type, select OAuth 2.0. This will display a list of the available tokens, as well as Postman's built-in functionality to Get New Access Token.


2. Click on the button Get New Access Token. This will display a form that will ask for various endpoints and information about the client app. The Auth URL and the Access Token URL are available from the well-known URL. The Client ID and Client Secret correspond to the ones you've configured in Keycloak. As we're using the pre-configured version from Baeldung, these values are fooClient and fooClientSecret, respectively.


3. Click on Request Token. A new window to log in, will be displayed. As we're using the configuration from Baeldung, the user and password combo is: john@test.com / 123
 

4. After logging in, the token will be registered in Postman and we can use it for future calls.

Now that we have a valid token right from our authorization server, is the time to test. This time, we're going to test the API users.

Test 1: No Token

The request does not contain any token. Therefore, the answer should be 401 Unauthorized. This is exactly the response we're obtaining from the server.

Test 2: Invalid token

Once again, we were expecting a response 401 Unauthorized and that's exactly what we got.


Test 3: Valid JWT
Last, we used the token we generated in the previous section. We expected a HTTP response 200, along with the message Here goes a secret list of users. That's exactly the response we got. 

And that concludes today's tutorial. Feel free to download and use the source code for this example from Github.

Comments