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

Callista medarbetare Björn Beskow

Managing APIs using Kong Gateway Part 3: Mutual TLS and Certificate-Bound Access Tokens

// Björn Beskow

In the first part, we implemented access control for a sample API using Kong plugins for OIDC and OAuth 2.0, and in the second part, we implemented access control using Mutual TLS. In this short post, we will combine OAuth 2.0 Client Credentials with Mutual TLS, which is a frequently used pattern.

Blog Series Parts

Certificate-Bound Access Tokens

When using OAuth 2.0 Client Credentials in IoT scenarios, client authentication towards the OAuth 2.0 token service based on x509 certificate using Mutual TLS is often required. For extra security, to mitigate the risk that a leaked access token is used to give access to an API to an attacker, an additional mechanism can be used to bind the access token to the client certificate to whom it was originally issued. The protected API thus requires a mutual TLS session to be used, alongside the use of an access token. If the token contains information about the intended client’s certificate, the API can verify that the token is not being misused by a bogus client.

Chain Photo by Bryson Hammer on Unsplash

RFC8705 specifies how mutual TLS can be used as an authentication mechanism when requesting access tokens. It also specifies how the client certificate “fingerprint” can be encoded in the access token, thereby binding the token to the specific client. The cnf.x5t#S256 property contains a sha256 digest of the client certificate:

  "sub": "service-account-system-client-mtls",
  "typ": "Bearer",
  "azp": "system-client-mtls",
  "cnf": {
    "x5t#S256": "4itBP0qzu-ZJAswWgV0dGyofr-Xqz8tfrhbxdecgev4"

Neither the Kong Enterprise oauth2-introspection plugin nor the Open Source kong-token-introspection that we used in part 1 supports validating a certificate-bound access token. They also are not flexible enough to meet a couple of common requirements: limit access to an API based on a set of required scopes that must be present in the access token, and making access token information such as username, client-id and scope easily available to the upstream API via custom http headers. All the actual x509 functionality is however provided by OpenResty, a Lua interface to Nginx on which Kong is based. Hence it is quite easy to build a custom Kong plugin which besides doing token introspection also can verify a client certificate digest against a certificate-bound access token.

I therefore wrote a simple token-introspection plugin in Lua, available as Open Source under the Apache license. If configured with the name of a header holding a client certificate and the access token is bound to a certificate as of RFC8705, the plugin will verify that the client certificate digest matches the digest from the access token. If configured with one or more scopes, the plugin will verify that the access token contains the configured scopes. If authorization is successful, the plugin will make a set of standard claims and optionally a set of custom claims from the access token available as http headers, for the upstream service to consume.

Let’s see how we can expose our sample API using mTLS and a certificate-bound access token! As before, a fully working, minimalistic example can be found in the Github repository.

Preliminary setup: Configure Keycloak for mTLS authentication with Certificate-Bound Access Tokens

We first update the Keycloak docker container to use https, using our self-signed PKI for both server certificate and trust store:


      - "9080:9080"
      - "9443:9443"
      KEYCLOAK_ADMIN: admin
      - start-dev
      - --db=dev-mem
      - --hostname=host.docker.internal
      - --http-port=9080
      - --https-port=9443
      - --https-certificate-file=/opt/keycloak/ssl/server/localhost.crt
      - --https-certificate-key-file=/opt/keycloak/ssl/server/localhost.key
      - --https-trust-store-file=/opt/keycloak/ssl/CA/localCA.p12
      - --https-trust-store-password=secret
      - --https-client-auth=request
      - --import-realm
      - ./keycloak-test-realm.json:/opt/keycloak/data/import/realm.json
      - ./certs:/opt/keycloak/ssl

Next, we add an OAuth 2.0 client that requires authentication using mTLS, again with the custom scope system-access:

  "users": [
      "id": "service-account-system-client-mtls",
      "username": "service-account-system-client-mtls",
      "enabled": true,
      "serviceAccountClientId": "system-client-mtls"
  "clients": [
      "clientId": "system-client-mtls",
      "enabled": true,
      "clientAuthenticatorType": "client-x509",
      "standardFlowEnabled": false,
      "serviceAccountsEnabled": true,
      "authorizationServicesEnabled": true,
      "protocol": "openid-connect",
      "attributes": {
        "x509.subjectdn": ".*O=Callista Enterprise AB,.*",
        "x509.allow.regex.pattern.comparison": "true",
        "tls.client.certificate.bound.access.tokens": "true"
      "defaultClientScopes": [
    "clientScopes": [
        "name": "system-access",
        "protocol": "openid-connect"

In the attributes section, we specify the client certificate details for the client. By using a regular expression for the x509.subjectdn value, we can again allow any system with a certificate issued for the Callista Enterprise AB organization to be able to use this client. We also specify that access tokens for this client should be bound to the client certificate.

Update the Docker image to include the token-introspection plugin

As before, we need to update the Dockerfile to bundle the token-introspection plugin:


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


and update kong.conf to load it:

plugins = bundled, oidc, mtls-auth, token-introspection

Configuring Kong to expose the upstream api with mTLS protection and Certificate-Bound Access Tokens

We finally add configuration in kong.yml to expose our sample API beneath the path /token-mtls/, with the mtls-auth and token-introspection plugins applied:

- name: access-token-mtls
  host:  upstream
  port: 80
  protocol: http
  - name: mtls-auth
      upstream_cert_header: "X-Client-Cert"
  - name: token-introspection
      introspection_endpoint: https://host.docker.internal:9443/realms/test/protocol/openid-connect/token/introspect
      client_id: introspection-client
      client_secret: secret
      certificate_header: "X-Client-Cert"
      - system-access
  - name: access-token-mtls-route
    - /token-mtls/
    strip_path: true

We use the mtls-auth plugin to extract the client certificate into the X-Client-Cert header, which is then used by the token-introspection plugin to validate against the digest in the bounded access token. We also require that the access token carries the system-access custom scope.

Testing the certificate-bounded access token flow

We are now ready to test the 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 with the Client Credentials flow, using the client1 client certificate as authentication:

> ACCESS_TOKEN=`curl --cacert certs/CA/localCA.crt --key certs/clients/client1.key --cert certs/clients/client1.crt  \
  --location --request POST 'https://host.docker.internal:9443/realms/test/protocol/openid-connect/token' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'client_id=system-client-mtls' \
  --data-urlencode 'grant_type=client_credentials' | jq -j '.access_token'`

Then call the api, again providing the client certificate to the TLS handshake and the access token as a header:

> curl -H "Authorization: Bearer $ACCESS_TOKEN" --cacert certs/CA/localCA.crt \
  --key certs/clients/client1.key --cert certs/clients/client1.crt \
  https://host.docker.internal:8443/token-mtls/api/ | jq .request.headers

In the echo’ed result, we can see the headers retrieved from the access token:

  "host": "upstream",
  "x-credential-scope": "system-access",
  "x-credential-client-id": "system-client-mtls",
  "x-credential-token-type": "Bearer",
  "x-credential-exp": "1683188977",
  "x-credential-iat": "1683188677",
  "x-credential-sub": "service-account-system-client-mtls",
  "x-credential-iss": "https://host.docker.internal:9443/realms/test",
  "x-credential-jti": "7f01ef9e-ce54-4221-a7ae-5868e9e2b8fc"

If we try to call the api using the same access token but another client certificate (e.g. client2), the mTLS handshake still succeeds, but since the client2 certificate’s digest differs from the digest bound to access token, access is denied by the token-introspection plugin:

> curl -v -H "Authorization: Bearer $ACCESS_TOKEN" --cacert certs/CA/localCA.crt \
  --key certs/clients/client2.key --cert certs/clients/client2.crt \
  https://host.docker.internal:8443/token-mtls/api/ | jq .request.headers
< HTTP/1.1 401 Unauthorized
  "data": [],
  "error": {
    "code": 401,
    "message": "The resource owner or authorization server denied the request."

Finally, shut down the containers:

> docker compose down

Summing up

In this post, we have extended the OAuth 2.0 Client Credentials flow with autentication using mutual TLS. By binding the access token to the client certificate to which the token was issued, we prevent the risk that a compromised access token can be misused by a bogus client. Just as in the previous examples, the result is simple, elegant and well encapsuled.

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

What’s next?

This post concludes these Access Control examples. In a forthcoming post (if time permits …), we will have a look at Rate Limiting using Kong Gateway. Stay tuned!


The following Kong plugin has been a very useful inspiration when writing the token-introspection plugin:

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.