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.