API Gateway Security - JWT

Mateusz Hinc
2.0.0

Overview

In this tutorial, we will take a look on how to secure an API gateway.

This tutorial is based on the code we implemented in Getting Started with Docker tutorial. If you didn't complete the tutorial, we highly encourage you to do so.

Prerequisites

  • JDK 8
  • Docker (not required, you can use Stack distribution as well)
  • curl

To keep things simple, we'll start from the code already implemented in the tutorial mentioned before, which is available here.

Let's quickly recap on the things we'll reuse here.

We've already implemented the HelloWorldHandlerFactory which simply prints the configured message to the HTTP response. All the details are available here in section API implementation: creating new Handler.

In this tutorial we'll make sure that this handler is accessible only for authenticated users.

JWT

A JWT (JSON Web Token) is a well-known solution for API authorization. The JWT allows verifying the owner of some JSON data. The JSON data stored in a JWT is serialized, not encrypted. So it can be seen by anyone (remember to always use HTTPS).

However, it is cryptographically signed by the authentication provider. So no one can modify it, it guarantees that can be trusted.

The JWT Authentication flow looks like this:

JWT flow

  1. A user logs in with his/her credentials.
  2. The user receives an encrypted JWT in a cookie tagged with the HttpOnly flag (it tells that only the server can access this particular cookie).
  3. From now, all requests within the same domain contain JWT token and secured API can verify them (those endpoints share the same signing key).

Please note that, for simplicity, we specify JWT in HTTP headers. Usually, there is some proxy front server (e.g. Apache or Nginx) that rewrites an authentication cookie to HTTP headers.

Since we'll be focusing only on step 3, we'll mimic the result of the other two by generating the token ourselves.

Configure JWT auth in OpenAPI

As always, we'll use the Design First approach, therefore we'll modify our Open API 3 configuration file.

You might have already noticed how we use swagger.io and OpenAPI references alternately, depending on what we find more readable for the situation. It's good to understand the difference between them:

OpenAPI is the specification and Swagger is a set of tools for implementing the specification.

For more information see this article.

Let's add a new endpoint in the paths: section and a new securityScheme in the components section:

knotx/conf/openapi.yaml

paths:
  /api/secure/jwt: // endpoint with JWT authentication
    get:
      operationId: hello-world-operation-jwt
      security:
        - helloJwtAuth: [] // here we specify which security scheme we'll be using (see below)
      responses:
        '200':
          description: Hello World API protected with JWT
        '401':
          description: Unauthorized access

Notice different responses defined for the secured endpoint. For more info on different responses see this tutorial.

Now let's create security scheme definitions. We'll place it in a separate place in the same file as above in components section. For now we'll have only one securityScheme called helloJwtAuth:

components:
  securitySchemes:
    helloJwtAuth: // our custom security scheme
      type: http
      scheme: bearer
      bearerFormat: JWT

For more information on bearer scheme see this documentation.

Now we'll take care of handling our operations and security schemas. Let's modify knotx/conf/routes/operations.conf:

routingOperations = ${routingOperations} [
  {
    operationId = hello-world-operation-jwt
    handlers = [
      {
        name = hellohandler
        config = {
          message = "Hello World From Knot.x with JWT!"
        }
      }
    ]
  }
]

As we can see, we use our hellohandler implemented in HelloWorldHandlerFactory, but with different configured message.

Now let's add security handlers (in the same file):

securityHandlers = [
  {
    schema = helloJwtAuth
    factory = helloJwtAuthFactory
    config = {
      algorithm = "HS256"
      publicKey = "M0NTY3ODkwIiwibmFtZSI6"
      symmetric = true
    }
  }
]

We provided an array of securityHandlers. It's a collection of objects which map schema with factory that must implement AuthHandlerFactory interface. We can also pass some config here.

We used HS256 with a symmetric key. A signature is generated by calculating a digest using the HMAC-SHA256 hashing algorithm. The key used in HMAC is, by definition, symmetric. A symmetric key is used for both to encrypt and decrypt JWT. So we need to configure two properties: symmetric and publicKey. More info available here.

Be careful not to disclose your private keys anywhere publicly!

It is worth noting that operations are not aware of any security they will be behind of!

Authentication Handler implementation

For the purpose of this tutorial let's add a new module to our project. Let's name it security-module and give it the following structure.:

modules
└─ security-module
    ├── build.gradle.kts                                                    // gradle build script
    └── src
        └── main
            ├── java/io/knotx/examples/security/auth
            │     └── JwtAuthHandlerFactory.java                            // the handler factory
            └── resources
                  └── META-INF/services
                      └── io.knotx.server.api.security.AuthHandlerFactory   // META-INF file used by Knot.x to find the handler

The modules/security-module/build.gradle.kts takes care of any dependencies we'll need in this tutorial:

dependencies {
    "io.knotx:knotx".let { v ->
        implementation(platform("$v-dependencies:${project.property("knotx.version")}"))
        implementation("$v-fragments-handler-api:${project.property("knotx.version")}")
    }
    "io.vertx:vertx".let { v ->
        implementation("$v-core")
        implementation("$v-rx-java2")
        implementation("$v-health-check")
        implementation("$v-auth-jwt")
    }
    "org.apache".let { v ->
        compile("$v.httpcomponents:httpclient:4.5.3")
        compile("$v.commons:commons-lang3:3.9")
    }
}

Let's not forget to add the module to the main settings.gradle.kts!

include("security-module")
// ...
project(":security-module").projectDir = file("modules/security-module")

Now let's take care of the actual authentication handler factory implementation:

JwtAuthHandlerFactory.java

package io.knotx.examples.security.auth;

import io.knotx.server.api.security.AuthHandlerFactory;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.auth.PubSecKeyOptions;
import io.vertx.ext.auth.jwt.JWTAuthOptions;
import io.vertx.reactivex.core.Vertx;
import io.vertx.reactivex.ext.auth.jwt.JWTAuth;
import io.vertx.reactivex.ext.web.handler.AuthHandler;
import io.vertx.reactivex.ext.web.handler.JWTAuthHandler;

public class JwtAuthHandlerFactory implements AuthHandlerFactory {

  @Override
  public String getName() {
    return "helloJwtAuthFactory";
  }

  @Override
  public AuthHandler create(Vertx vertx, JsonObject config) {
    PubSecKeyOptions pubSecKey = new PubSecKeyOptions(config);
    JWTAuthOptions jwtAuthOptions = new JWTAuthOptions().addPubSecKey(pubSecKey);
    return JWTAuthHandler.create(JWTAuth.create(vertx, jwtAuthOptions));
  }
}

We're creating a configuration based on the config JSON the factory receives from operations.conf and return a JWTAuthHandler that does all the authentication for us.

Let's not forget to register our factory for Java ServiceLoader:

io.knotx.server.api.security.AuthHandlerFactory

io.knotx.examples.security.auth.JwtAuthHandlerFactory

Build & Run

In this tutorial we'll be using the Docker distribution, but it will work with Stack distribution as well. Please refer to this tutorial on how to work with the Stack distribution.

First, let's rename the Docker image we're about to create:

gradle.properties

// ...
docker.image.name=knotx-example/secure-api-gateway
// ...

Now let's build the image:

$ gradlew clean build-docker

and run it:

$ docker run -p8092:8092 knotx-example/secure-api-gateway

After a while the Docker container should be up and running.

Let's try accessing our endpoints:

$ curl -X GET http://localhost:8092/api/secure/jwt
Unauthorized

We were not authorized to access this endpoint.

We need an Authorization header for our request. As mentioned before, we'll mimic a successful login and we'll generate the token ourselves.

There are many ways to generate a token. We'll use jwt.io.

Open the page, scroll to the Debugger section and generate the token:

Generating JWT using jwt.io

  1. Select HS256 as the algorithm.
  2. Fill in the key you used in the configuration.
  3. Copy the generated token.

Now let's try this curl command with Authorization header:

$ curl -X GET http://localhost:8092/api/secure/jwt -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.vPWK59pl5GWimz8UVbL3CmrceSfmNvvCgyzwLVV9jT8'
{"message":"Hello World From Knot.x with JWT!"}

Functional tests

It's always a good practice to test our code. Let's implement some tests that will automatically do the same things we did manually above.

The tests we're about to write will be functional tests, that will be executed on a real, running Docker image, just after the healthcheck (described in this tutorial) executes successfully.

Let's add a new file: functional/src/test/java/com/project/test/functional/JWTAuthITCase.java

class JWTAuthITCase {

  @Test
  @DisplayName("GIVEN no authorization WHEN call JWT secured API EXPECT Unauthorized")
  void givenNoAuthorizationWhenCallBasicAuthApiExpectUnauthorized() {
    given()
        .port(8092)
      .when()
        .get("/api/secure/jwt")
      .then()
        .assertThat()
        .statusCode(401);
  }

  @Test
  @DisplayName("GIVEN authorization WHEN call JWT secured API EXPECT Ok")
  void givenAuthorizationWhenCallBasicAuthApiExpectOk() {
    given()
        .port(8092)
        .header("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.vPWK59pl5GWimz8UVbL3CmrceSfmNvvCgyzwLVV9jT8")
      .when()
        .get("/api/secure/jwt")
      .then()
        .assertThat()
        .statusCode(200);
  }
}

We've created two simple tests using REST-assured. They do the same actions we did above:

  • call secured endpoint without authorization and expect 401 - Unauthorized response
  • call the same endpoint with authorization and expect 200 - OK response

If you try rebuilding the project now (remember to stop any running containers before that) you'll notice that those test will be invoked. Now we're confident that if our future changes will somehow disable or modify our /api/secure/jwt logic, we'll know at build time!

Summary

In this tutorial we have successfully secured our API Gateway with one of the most common authentication methods.

The only thing left to do here for a complete application is to implement login functionality. After successful login, the token should be stored by the browser's cookie in a manner that the token is appended to every request, but is not available for the front-end logic. Lastly, our proxy (e.g. Apache or Nginx) should rewrite this cookie to a header before passing a request to Knot.x.


You can find full project implementation here.