Integrating with Kong Open Source

Integrating with Kong Open Source

tutorials

Overview

In this article we will show how to use Kong Open Source as an API Gateway with the Curity Identity Server, and we’ll use a small LUA plugin to implement the Phantom Token Pattern. This will result in an end to end solution with the following behavior:

  • An OAuth Client will get an opaque access token
  • We will send the opaque access token to an API via Kong
  • The opaque token will be introspected to get a JWT, which will be forwarded to the API
  • The API will successfully validate the JWT and return some data

Run the Default Kong Docker Image

First ensure that Docker Desktop is installed locally as a prerequisite. Next create a local folder called kong and add a minimal docker-compose.yml file:

version: '3.8'
services:
  my_kong:
    image: kong:2.4.1-alpine
    ports:
    - 8080:8080
    - 8081:8081
    volumes:
    - ./kong.yml:/usr/local/kong/declarative/kong.yml
    environment:
      KONG_DATABASE: 'off'
      KONG_DECLARATIVE_CONFIG: '/usr/local/kong/declarative/kong.yml'
      KONG_PROXY_LISTEN: '0.0.0.0:8080'
      KONG_ADMIN_LISTEN: '0.0.0.0:8081'

For this tutorial we will use a DB-less and Declarative Configuration which provides the simplest local deployment options. A kong.yml file is needed, which will initially contain only some default content:

_format_version: '2.1'
_transform: true

We can then run Kong with the following command, which will download the docker image:

docker-compose up --force-recreate

We have configured the Kong API Gateway to listen over port 8080 and there is also an admin endpoint that we can browse to at http://localhost:8081, to visualize the system settings:

Kong Admin

Deploy the Phantom Token Plugin

Next we will extend the configuration in order to include a phantom token plugin, so first download the plugin from the Curity Github Repository:

git clone https://github.com/curityio/kong-phantom-token-plugin

Update the docker-compose.yml file to copy the plugin files into the container’s LUA plugins location and to activate the plugin:

version: '3.8'
services:
  my_kong:
    image: kong:2.4.1-alpine
    ports:
    - 8080:8080
    - 8081:8081
    volumes:
    - ./kong.yml:/usr/local/kong/declarative/kong.yml
    - ./kong-phantom-token-plugin/plugin:/usr/local/share/lua/5.1/kong/plugins/phantom-token
    environment:
      KONG_DATABASE: 'off'
      KONG_DECLARATIVE_CONFIG: '/usr/local/kong/declarative/kong.yml'
      KONG_PROXY_LISTEN: '0.0.0.0:8080'
      KONG_ADMIN_LISTEN: '0.0.0.0:8081'
      KONG_PLUGINS: 'bundled,phantom-token'

Also edit the kong.yml file, to add an API route that points to an API we will run at port 3000 on the host. The plugin is configured to point to the Curity Identity Server’s introspection endpoint on the host, and this configuration assumes that the default ports are being used.

Note also that in order for the Kong docker container to be able to route requests to the host, we are using a host name of host.docker.internal, which points to localhost on the host computer:

_format_version: '2.1'
_transform: true

services:
- name: demo-api
  url: http://host.docker.internal:3000/api
  routes:
  - name: demo-api-route
    paths:
    - /api
  plugins:
  - name: phantom-token
    config:
      introspection_endpoint: http://host.docker.internal:8443/oauth/v2/oauth-introspect
      client_id: api-gateway-client
      client_secret: Password1
      token_cache_seconds: 900
      scope: read

A this point the local kong folder should contain the following files, and we can re-run the docker-compose command to start Kong with the custom plugin activated:

Kong Files

The following commands can be used from another terminal to confirm that the plugin and configuration have been correctly deployed within the running container:

CONTAINER_ID=$(docker container ls | grep my_kong | awk '{print $1}')
docker exec -it $CONTAINER_ID sh -c "ls /usr/local/share/lua/5.1/kong/plugins/phantom-token"
docker exec -it $CONTAINER_ID sh -c "cat /usr/local/kong/declarative/kong.yml"

Run an API on the Host

We will run an API on the host, and call it via port 8080, so that the request is routed via Kong. The phantom token plugin will introspect incoming access tokens to get a JWT, then forward them to the real API endpoint, at http://localhost:3000/api/data.

If you prefer, you can choose a different technology from the API Code Examples, or plug in your own API. This tutorial will use the Kotlin Example API, which you can get with these commands:

git clone https://github.com/curityio/kotlin-api-jwt-validation

Next edit the API’s api.properties file to ensure that it points to your instance of the Curity Identity Server:

port=3000
jwks_endpoint=http://localhost:8443/oauth/v2/oauth-anonymous/jwks
issuer_claim=http://localhost:8443/oauth/v2/oauth-anonymous
audience_claim=api.example.com

Then run the API, and this particular sample requires Maven and Java 8+ to run:

cd kotlin-api-jwt-validation
mvn package
java -jar target/secureapi-1.0-SNAPSHOT.jar

The docker container will route requests to these URLs on the host, and for this to work we configured a host name of host.docker.internal, which points to localhost on the host computer:

URLDescription
http://localhost:8443/oauth/v2/oauth-introspectThe introspection endpoint of the Curity Identity Server
http://localhost:3000/api/dataThe sample API endpoint which will receive a digitally signed JWT

We can now call the sample API with the following command, though we are not supplying a real token to Kong, so the request will fail OAuth validation:

curl -i -H "Authorization: Bearer abc123" http://localhost:8080/api/data

As expected, this results in an introspection failure, and you can understand the cause from the phantom token plugin’s log output, which is routed to stderr:

Introspect 401

Plugin Implementation Details

There are plenty of online tutorials on Writing LUA plugin for Kong, and only the following simple logic is needed to implement the phantom token pattern:

  • Read the opaque access token from the incoming request’s HTTP Authorization Header
  • Call the introspection endpoint, sending an accept: application/jwt header
  • Update the API request’s HTTP Authorization Header with the JWT before routing it
  • Cache the JWT for future requests with the same access token, using Kong’s built in cache

With LUA and Kong this is quite simple to code, and the access.lua file contains the details of the OAuth processing code:

function _M.run(config)

    if ngx.req.get_method() == "OPTIONS" then
        return
    end

    local access_token = ngx.req.get_headers()["Authorization"]
    if access_token then
        access_token = pl_stringx.replace(access_token, "Bearer ", "", 1)
    end

    if not access_token then
        ngx.log(ngx.WARN, "No access token was found in the Authorization bearer header")
        invalid_token_error_response()
    end

    local res = verify_access_token(access_token, config)
    local jwt = res.body

    if not verify_scope(jwt, config.scope) then
        error_response(ngx.HTTP_FORBIDDEN, "forbidden", "The token does not contain the required scope: " .. config.scope)
    end

    ngx.log(ngx.INFO, "The request was successfully authorized by the gateway")
    ngx.req.set_header("Authorization", "Bearer " .. jwt)
end

Test the End to End Flow

Next we need to configure a simple OAuth client in order to get an opaque access token, and you can use a client of your choice. We will use a simple Client Credentials client that uses a read scope. An additional introspection client is also required, and is used by the phantom token plugin in the Kong docker container. The combined configuration XML for both clients is shown below:

<client>
    <id>api-client</id>
    <client-name>api-client</client-name>
    <secret>Password1</secret>
    <audience>api.example.com</audience>
    <scope>read</scope>
    <capabilities>
        <client-credentials/>
    </capabilities>
    <use-pairwise-subject-identifiers>
        <sector-identifier>api-client</sector-identifier>
    </use-pairwise-subject-identifiers>
</client>
<client>
    <id>api-gateway-client</id>
    <client-name>api-gateway-client</client-name>
    <secret>Password1</secret>
    <capabilities>
        <introspection/>
    </capabilities>
    <use-pairwise-subject-identifiers>
        <sector-identifier>api-gateway-client</sector-identifier>
    </use-pairwise-subject-identifiers>
</client>

We can now get an opaque access token via the following simple curl request:

curl -u 'api-client:Password1' -X POST http://localhost:8443/oauth/v2/oauth-token \
-d grant_type=client_credentials \
-d scope=read

Take the access token from the response and then run a second curl request to send a valid token through Kong to our API:

curl -H "Authorization: Bearer e4d51903-0f9d-41e5-b50e-b7fe7d630612" http://localhost:8080/api/data

You should now see a successful end to end flow, where the Kotlin API validates the JWT and echoes back some scopes and claims contained in it:

Introspection Success

For debugging purposes it can also be useful to test the introspection manually from the host, with the following command. Notice that a special form of OAuth introspection is used, with accept=application/jwt, so that the response is a digitally signed JWT and not a JSON object:

curl -u 'api-gateway-client:Password1' \
-H "accept: application/jwt" \
-X POST http://localhost:8443/oauth/v2/oauth-introspect \
-d token=e4d51903-0f9d-41e5-b50e-b7fe7d630612

Handle CORS Requests

If you are making cross domain API requests from an OAuth secured Single Page Application (SPA), you will also need to ensure that any error responses from the plugin are readable by the SPA. This can be done by configuring trusted web origins as follows:

_format_version: '2.1'
_transform: true

services:
- name: demo-api
  url: http://host.docker.internal:3000/api
  routes:
  - name: demo-api-route
    paths:
    - /api
  plugins:
  - name: phantom-token
    config:
      introspection_endpoint: http://host.docker.internal:8443/oauth/v2/oauth-introspect
      client_id: api-gateway-client
      client_secret: Password1
      token_cache_seconds: 900
      scope: read
      trusted_web_origins:
      - http://www.example.com

Conclusion

The phantom token pattern can be quickly implemented in an API Gateway, since it typically only requires a small plugin. Once this is working on a development computer it is then easy to use Docker in the same way to publish to deployed environments. Using the phantom token approach results in a secure solution, where no sensitive token details are exposed to internet clients.

Keep up with our latest articles and how-tos RSS feeds.