API Gateway Security - Basic Auth

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.

Basic Auth

Basic auth is the most basic way of authenticating your requests. It's a simple username/password credential driven security.

Configure basic 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/basic: // endpoint with basic authentication
    get:
      operationId: hello-world-operation-basic
      security:
        - helloBasicAuth: [] // here we specify which security scheme we'll be using (see below)
      responses:
        '200':
          description: Hello World API protected with Basic Auth
        '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 helloBasicAuth:

components:
  securitySchemes:
    helloBasicAuth: // our custom security scheme ...
      type: http  // ... with a http type ...
      scheme: basic   // ... with a Basic Auth scheme

For more information on basic 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-basic
    handlers = [
      {
        name = hellohandler
        config = {
          message = "Hello World From Knot.x with Basic Auth!"
        }
      }
    ]
  }
]

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

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

securityHandlers = [
  {
    schema = helloBasicAuth
    factory = helloBasicAuthFactory
    config = {
      properties_path = "classpath:basicauth/users.properties"
    }
  }
]

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 (more on that later).

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
            │     └── BasicAuthHandlerFactory.java                          // the handler factory
            └── resources
                  ├── basicauth
                  │   └── users.properties                                  // our config file
                  └── 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-shiro")
    }
    "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 focus on the resources directory.

Let's get back to the securityHandlers we defined before. The configuration we pass to helloBasicAuthFactory is a path to user/roles configuration file.

In this example we'll be using Vert.x's implementation of Apache Shiro Auth for handling users, their credentials and privileges. Therefore, the configuration must be in a format described here.

As mentioned before, we'll create the file in module's resources/basicauth folder and name it users.properties.

Let's create a sample user john with a super secure password. He'll have an administrator role which has all the permissions (*):

users.properties

user.john = s3cr3t,administrator
role.administrator=*

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

BasicAuthHandlerFactory.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.shiro.ShiroAuthOptions;
import io.vertx.reactivex.core.Vertx;
import io.vertx.reactivex.ext.auth.shiro.ShiroAuth;
import io.vertx.reactivex.ext.web.handler.AuthHandler;
import io.vertx.reactivex.ext.web.handler.BasicAuthHandler;

public class BasicAuthHandlerFactory implements AuthHandlerFactory {

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

  @Override
  public AuthHandler create(Vertx vertx, JsonObject config) {
    final ShiroAuth shiroAuth = ShiroAuth.create(vertx, new ShiroAuthOptions().setConfig(config));
    return BasicAuthHandler.create(shiroAuth);
  }
}

It's as simple as creating a BasicAuthHandler that will use ShiroAuth instance created with our config.

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

io.knotx.server.api.security.AuthHandlerFactory

io.knotx.examples.security.auth.BasicAuthHandlerFactory

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 endpoint:

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

As expected - we were not authorized to access this endpoint.

Let's try again with a valid Authorization header. The header should be a Base64-encoded string of pair of username:password prepended with Basic keyword, as described here:

$ curl -X GET http://localhost:8092/api/secure/basic -H 'Authorization: Basic am9objpzM2NyM3Q='
{"message":"Hello World From Knot.x with Basic Auth!"}

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/BasicAuthITCase.java

class BasicAuthITCase {

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

  @Test
  @DisplayName("GIVEN authorization WHEN call basicAuth API EXPECT Ok")
  void givenAuthorizationWhenCallBasicAuthApiExpectOk() {
    given()
        .port(8092)
        .header("Authorization", "Basic am9objpzM2NyM3Q=")
      .when()
        .get("/api/secure/basic")
      .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/basic logic, we'll know at build time!

Summary

In this tutorial we have successfully secured our API Gateway with the most basic authentication method.

Using Basic Auth we can set up credential-based security in no time! The beauty of this is that the business logic is not aware of the security layer standing right in front of it.


You can find full project implementation here.