Blogg

Här finns tekniska artiklar, presentationer och nyheter om arkitektur och systemutveckling. Håll dig uppdaterad, följ oss på LinkedIn

Callista medarbetare Björn Beskow

Managing APIs using Kong Gateway Part 1: Securing an API using OIDC and OAuth 2.0

// Björn Beskow

API gateways are becoming increasingly more popular, and for good reasons. As the number of APIs within an organisation grows, the amount of “plumbing” required to expose the APIs in a secure, efficient and maintainable way quickly becomes overwhelming. An API Gateway is an architectural pattern which introduces a transparent placeholder between API clients and the APIs, where Cross Cutting Concerns such as Access Control, Monitoring, Logging, Caching and Rate Limiting can be implemented. In this blog series, we’ll be demonstrating how to use Kong, one of the leading Open Source API Gateways, to add various common capabilities to an API.

Blog Series Parts

API Gateways

An API Gateway product is a software component or framework that implements the API Gateway pattern. It acts as a common entry point for underlying APIs, and allows various common capabilities to be applied to the API interactions. By externalizing common, cross-cutting concerns (such as enforcing an autentication and authorization policy) a uniform and well tested implementation can be used for multiple APIs, while at the same time simplifying the underlying APIs. Technically, the API Gateway functions as a Reverse Proxy, and not surprisingly, several of the Open Source API Gateway products are built on top of a solid Reverse Proxy product.

Gateway Photo by Jason Dent on Unsplash

While the big Cloud vendors provide their own API Gateways as part of more ambitions API Management Platforms, there are several Open Source API Gateway alternatives that have the additional benefit of being cloud/on premise agnostic. One of the more popular Open Source choices is Kong Gateway. As with many OS products, the community edition is free for use but lacks some of the premium features found in the Enterprise edition. The community edition is however capable enough to be an excellent starting point for getting the toes wet with API Management.

Authentication and Authorization using Kong Gateway

In this first post, we’ll show how to use the Kong Gateway to enforce a couple of different authentication and authorization strategies:

  • End user authentication and authorization using OpenID Connect.
  • Server authentication and authorization using OAuth 2.0 Client Credentials.

As usual, we’ll use docker containers and docker compose to minimize the installation requirements while hiding irrelevant details to focus on the essential configuration required. The fully working example can be found in the Github repository. Note: Since the project uses Git submodules, it should be checked out with the --recursive flag to recursively also fetch the submodules:

git clone --recursive https://github.com/callistaenterprise/blog-api-gateway-kong

End user authentication and autorization using OpenID Connect

Modern APIs that rely on end user’s access rights typically uses OAuth 2.0 with OpenID Connect (OIDC) for authentication and authorization. Details about OAuth 2.0 and OIDC can be found in numerous blogs (see e.g. curity.io), so we won’t repeat that here. It is sufficient to say that a mechanism for enforcing OIDC-based authentication and authorization must be able to detect sessions that are not already authenticated, and redirect those users to an appropriate login mechanism. Once the user has authenticated, the resulting access token (and potentially also a corresponding refresh token) should be managed and forwarded to the underlying API. No API calls must be allowed without a valid access token.

Instagram Photo by Mourizal Zativa on Unsplash

In Kong Gateway, externalizing a cross-cutting concern such as this is done using a Plugin which is declaratively configured to be applied to one or more Services or Routes or globally. Plugins in Kong are normally written in Lua, and the different Kong editions comes with different subsets of standard plugins (see Kong Hub). While the official Kong openid-connect plugin is only available in the Kong Enterprise Edition, there is a lightweight and excellent OS alternative kong-oidc plugin maintained by Nokia which implements the OIDC Relying Party functionality.

Preliminary setup: An example upstream API

For simplicity, we’ll use an existing docker image to simulate an upstream API, onto which we shall apply access control. The echo-server implements a generic http-based api, echoing request headers and body back to the caller:

services:

  upstream:
    image: ealen/echo-server

The API is available on port 80, but we are not exposing this port on the host network. It should only be available via the Kong gateway that we are about to add soon (which will use the docker-internal upstream:80 address to proxy the API).

Preliminary setup: An example OAuth 2.0/OIDC server

We’ll use a Keycloak docker container as OAuth 2.0/OIDC server.

services:

  ...
  
  keycloak:
    image: quay.io/keycloak/keycloak
    ports:
      - "9080:9080"
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
    command: 
      - start-dev 
      - --db=dev-mem
      - --hostname=host.docker.internal
      - --http-port=9080
      - --import-realm
    volumes:
      - ./keycloak-test-realm.json:/opt/keycloak/data/import/realm.json

As per the spec, we need to configure an OAuth client to be used by the Kong oidc plugin for interacting with Keycloak. For implicity, we define the client in a realm test that we import on Keycloak startup:

  ...
  "clients": [
    {
      "clientId": "oidc-client",
      "enabled": true,
      "clientAuthenticatorType": "client-secret",
      "secret": "secret",
      "redirectUris": [
        "http://host.docker.internal:8080/oidc/*"
      ],
      "standardFlowEnabled": true,
      "protocol": "openid-connect"
    }
    ...

We also add a testuser user with password secret:

  "users": [
	{
	  "username" : "testuser",
	  "enabled" : true,
	  "credentials" : [
		  {
	        "type": "password",
	        "value": "secret"
	  ]
	}
	...
Preliminary setup: A network name known both to the docker compose network and in a browser running on localhost

In this setup, an extra complicating factor is the fact that the network addresses of the OAuth 2.0 server must be reachable both within the docker compose network (in order for the kong plugin to interact with the OAuth server) and on the localhost network (in order for a browser to be directed to an OIDC login page). The trick we will use is the magical host name host.docker.internal which automatically resolves to the docker host’s network within docker. We just have to make that sure that this hostname also resolves to localhost from the browser. We do that by adding it as an explicit entry to the /etc/hosts file:

echo '127.0.0.1 host.docker.internal' | sudo tee -a /etc/hosts > /dev/null
Configuring a Docker image for running Kong with a third-party plugin

Running a Kong docker container with a custom plugin such as the kong-oidc plugin requires a custom docker image where the plugin has been added. For this example, the kong-with-plugins folder contains a Dockerfile used to build the custom image, whereas the kong-with-plugins/plugins subfolder contains a git submodule which embeds the kong-oidc lua source code. The following Dockerfile produces the image:

FROM kong:2.8.3
USER root

COPY ./plugins/kong-oidc /custom-plugins/kong-oidc
WORKDIR /custom-plugins/kong-oidc
RUN luarocks make

USER kong
WORKDIR /

Note: If the kong-with-plugins/plugins/kong-oidc folder is empty, you may have initialize the submodules explicitly first:

git submodule update --init --recursive
Configuring Kong to expose the upstream api with OIDC protection

We can now configure the Kong container to expose our sample API behind the OIDC plugin. We need to provide two configuration files: kong.conf provides the technical configuration for the container image (network ports and plugins):

# listening ports
proxy_listen = 0.0.0.0:8080
admin_listen = 0.0.0.0:8081

# enabled plugins
plugins = bundled, oidc

For the routing configuration, we’ll use the Kong declarative configuration in DB-less mode and provide a kong.yml yaml configuration file. In the configuration, we specify that our upstream api should be available beneath the path /oidc/, with the kong-oidc plugin applied:

_format_version: "2.1"
_transform: true

services:
- name: oidc
  host:  upstream
  port: 80
  protocol: http
  plugins: 
  - name: oidc
    config: 
      discovery: http://host.docker.internal:9080/realms/test/.well-known/openid-configuration
      client_id: oidc-client
      client_secret: secret
  routes:
  - name: oidc-route
    paths:
    - /oidc/
    strip_path: true

The oidc plugin is configured with OIDC discovery url to Keycloak, as well as the client and secret to use. The /oidc/ path prefix will be removed before delegating to the upstream API.

Finally, we add the Kong gateway service to our docker compose file, which now looks like this:

version: '3.9'

services:

  upstream:
    image: ealen/echo-server

  keycloak:
    image: quay.io/keycloak/keycloak
    ports:
      - "9080:9080"
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
    command: 
      - start-dev 
      - --db=dev-mem
      - --hostname=host.docker.internal
      - --http-port=9080
      - --import-realm
    volumes:
      - ./keycloak-test-realm.json:/opt/keycloak/data/import/realm.json

  kong:
    image: kong-with-plugins
    build:
      context: ./kong-with-plugins
    user: root
    volumes:
      - ./kong.yml:/etc/kong/kong.yml
      - ./kong.conf:/etc/kong/kong.conf
    environment:
      KONG_DATABASE: "off"
      KONG_DECLARATIVE_CONFIG: "/etc/kong/kong.yml"
    ports:
      - "8080:8080"
      - "8081:8081"
Testing the OIDC flow

We are now ready to test the Kong Gateway in action. Start up the containers using docker compose in a terminal window:

docker compose up -d

Then open up a browser, and direct it to our API, via the Kong Gateway at http://host.docker.internal:8080/oidc/api/. You should be redirected to Keycloak for login.

login

Log in as testuser, and you should be redirected back to the upstream api. The oidc plugin maintains the user session, and passes on the OAuth 2.0 access token to the api in the x-access-token request header, the OIDC id token in x-id-token and the user info in x-userinfo:

loggedin

Finally, shut down the containers:

docker compose down

System authentication using OAuth 2.0 Client Credentials

When authenticating a system actor using OAuth 2.0, the Client Credentials flow is used. In this scenario, we don’t need the full redirect and session maintenance model of OIDC. The calling system obtains an access token from the OAuth server beforehand, and provides it as an Authorization header for all subsequent API calls. Hence to protect the API, we just need to verify that a valid access token is present in each request. The Kong enterprise edition contains a full-fledged oauth2-introspection plugin, but we can use the simpler Open Source kong-token-introspection plugin for the same purpose.

Lock

Preliminary setup: Add a system client with a specific claim

First, we add another OAuth 2.0 client for our calling system to use, with the custom scope system-access:

  "users": [
    ...
    {
      "id": "service-account-system-client",
      "username": "service-account-system-client",
      "enabled": true,
      "serviceAccountClientId": "system-client"
    },
    ...
  ],
  "clients": [
     ...
    {
      "clientId": "system-client",
      "enabled": true,
      "clientAuthenticatorType": "client-secret",
      "secret": "secret",
      "serviceAccountsEnabled": true,
      "authorizationServicesEnabled": true,
      "protocol": "openid-connect",
      "defaultClientScopes": [
        "system-access"
      ]
    },
    ...
  ],
    "clientScopes": [
      {
        "name": "system-access",
        "protocol": "openid-connect"
      },
      ...
  ]
Preliminary setup: Add an introspection client

We also need to add an OAuth 2.0 client to be used by the kong-token-introspection plugin in order to validate access tokens with the OAuth server:

  "clients": [
  ...
    {
      "clientId": "introspection-client",
      "enabled": true,
      "clientAuthenticatorType": "client-secret",
      "secret": "secret",
      "serviceAccountsEnabled": true,
      "authorizationServicesEnabled": true,
      "protocol": "openid-connect"
    }
    ...
Update the Docker image to include kong-token-introspection

We need to update the Dockerfile to bundle the kong-token-introspection plugin:

...

COPY ./plugins/kong-token-introspection /custom-plugins/kong-token-introspection
WORKDIR /custom-plugins/kong-token-introspection
RUN luarocks make

...

and update kong.conf to load it:

plugins = bundled, oidc, access-token-introspection
Configuring Kong to expose the upstream api with OAuth 2.0 Access Token protection

We then add configuration in kong.yml to expose our sample API beneath the path /token/, with the kong-token-introspection plugin applied:

services:
...
- name: access-token
  host:  upstream
  port: 80
  protocol: http
  plugins: 
  - name: access-token-introspection
    config:
      introspection_endpoint: http://host.docker.internal:9080/realms/test/protocol/openid-connect/token/introspect
      client_id: introspection-client
      client_secret: secret
      scope: system-access
  routes:
  - name: access-token-route
    paths:
    - /token/
    strip_path: true

The plugin is configured with the introspection url to Keycloak, as well as the client and secret to use. We also require the access token to contain the scope system-access in order to allow access.

Testing the Client Credentials flow

We are now ready to test the Client Credentials flow. Rebuild the docker image, and start up the containers using docker compose in a terminal window:

docker compose build
docker compose up -d

Retrieve an access token from Keycloak using the Client Credentials flow

http://host.docker.internal:9080/realms/test/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=system-client' \
--data-urlencode 'client_secret=secret' \
--data-urlencode 'grant_type=client_credentials

The result is a json object containing the access token with additional meta data:

{"access_token":"....","expires_in":300,"refresh_expires_in":0,"token_type":"Bearer","not-before-policy":0,"scope":"profile email"}

For convenience, use jq to extract the access token and store it as a shell variable:

ACCESS_TOKEN=`curl -k --location --request POST 'http://host.docker.internal:9080/realms/test/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=system-client' \
--data-urlencode 'client_secret=secret' \
--data-urlencode 'grant_type=client_credentials' | jq -j .access_token`

Then use it to call the api, providing the access token as a header:

curl -H "Authorization: Bearer $ACCESS_TOKEN" http://host.docker.internal:8080/token/api/ | jq .request.headers

In the echo’ed result, we can see that the sub property from the access token is passed as header x-credential-sub and the scope property as x-credential-scope:

{
  "host": "upstream",
  ...
  "x-credential-sub": "service-account-system-client",
  "x-credential-scope": "system-access"
}

If we try to call the api without a valid access token, access is denied:

curl http://host.docker.internal:8080/token/api/ | jq .

{
  "data": [],
  "error": {
    "code": 401,
    "message": "Unauthenticated."
  }
}

Finally, shut down the containers:

docker compose down

Summing up

In this post, we have seen how to declaratively add access control based on OAuth/OIDC to an arbitrary API using Kong Gateway. It clearly demonstrates the value of the API Gateway pattern: the ability to externalize common behaviours from APIs and realize them in a single place is both simple and elegant, and allows for uniform, well encapsuled and tested realizations of important behaviours.

A fully working, minimalistic example can be found in the Github repository.

What’s next?

In the next part, we’ll expand the usage to examplify authentication and authorization using Mutual TLS and Access Control Lists. Stay tuned!

References

The following article has been a very useful inspiration when preparing this material:

https://levelup.gitconnected.com/implement-kong-as-a-dockerized-openid-connect-relying-party-community-edition-6e6a1ac5f05c

Tack för att du läser Callistas blogg.
Hjälp oss att nå ut med information genom att dela nyheter och artiklar i ditt nätverk.

Kommentarer