Dynamically set host based on the authenticated Principal with Datakit

TL;DR

Store the backend address as metadata on a Kong Identity Principal and link each OAuth client to its Principal with an auth_server_client identity. Configure the OpenID Connect plugin with principals.enabled set to true so it hydrates the Principal after verifying the token. Then configure the Datakit plugin to read kong.client.principal, pull the address out of the Principal’s metadata, and write it to kong.service.target.

Prerequisites

This is a Konnect tutorial and requires a Konnect personal access token.

  1. Create a new personal access token by opening the Konnect PAT page and selecting Generate Token.

  2. Export your token to an environment variable:

    export KONNECT_TOKEN='YOUR_KONNECT_PAT'
  3. Run the quickstart script to automatically provision a Control Plane and Data Plane, and configure your environment:

    curl -Ls https://get.konghq.com/quickstart | bash -s -- -k $KONNECT_TOKEN \
         --deck-output

    This sets up a Konnect Control Plane named quickstart, provisions a local Data Plane, and prints out the following environment variable exports:

    export DECK_KONNECT_TOKEN=$KONNECT_TOKEN
    export DECK_KONNECT_CONTROL_PLANE_NAME=quickstart
    export DECK_KONNECT_ADDR=https://us.api.konghq.com
    export KONNECT_CONTROL_PLANE_URL=https://us.api.konghq.com
    export KONNECT_PROXY_URL='http://localhost:8000'

    Copy and paste these into your terminal to configure your session.

To complete this tutorial, install decK. We recommend keeping decK up to date with the latest version (1.66.1).

decK is a CLI tool for managing Kong Gateway declaratively with state files. This guide uses deck gateway apply, which directly applies entity configuration to your Gateway instance.

You can check your current decK version with deck version.

For this tutorial, you’ll need Kong Gateway entities, like Gateway Services and Routes, pre-configured. These entities are essential for Kong Gateway to function but installing them isn’t the focus of this guide. Follow these steps to pre-configure them:

  1. Run the following command:

    echo '
    _format_version: "3.0"
    services:
      - name: example-service
        url: http://httpbin.konghq.com/anything
    routes:
      - name: example-route
        paths:
        - "/anything"
        service:
          name: example-service
        protocols:
        - http
        - https
    ' | deck gateway apply -

To learn more about entities, you can read our entities documentation.

A directory is a regional collection of principals. A Konnect organization supports only one Kong Identity directory. Create a directory for the tutorial, or look up your existing one.

Kong’s router runs before authentication, so it can’t directly route traffic based on who is making a request. This guide solves that using Kong Identity and two plugins working together in the access phase:

  1. You store each caller’s backend address as Principal metadata in Kong Identity.
  2. OpenID Connect verifies the bearer token and hydrates the matching Principal, including its metadata.
  3. Datakit reads the Principal, pulls the address out of its metadata, and sets it as the backend target for that request.

All callers share one Route and one Service, and the Datakit plugin decides dynamically which backend responds after authentication.

Because the routing map lives in Kong Identity, you create a new caller using the Principal API. You don’t need to edit the Datakit configuration or run a decK sync.

This guide routes directly to a host:port backend, and bypasses Upstream entities, load balancing, health checks, and retries. Use it when each backend is a fixed address and you don’t need a pool.

Note: The OpenID Connect plugin has a higher static priority than Datakit, so it always runs first in the access phase. No explicit plugin ordering configuration is required.

Create an authorization server in Kong Identity

An authorization server in Kong Identity issues the OAuth tokens that callers present to authenticate to your service. We recommend that you create different authorization servers for different environments. The authorization server name is unique per each organization and each Konnect region.

Create an authorization server using the /v1/auth-servers endpoint:

_response=$(curl -X POST "https://us.api.konghq.com/v1/auth-servers" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $KONNECT_TOKEN"\
     -H "Content-Type: application/json" \
     --json '{
       "name": "datakit-routing",
       "description": "Authorization server for Datakit host routing",
       "audience": "http://myhttpbin.dev"
     }')

Export the env variables:

export AUTH_SERVER_ID=$(echo "$_response" | jq -r ".id")
export ISSUER_URL=$(echo "$_response" | jq -r ".issuer")

Create the clients

Create one client per caller. Each client is the machine-to-machine credential that a caller uses to fetch a token. In this example, Konnect autogenerates the client ID and secret, but you can also specify them yourself instead.

Create the orders-caller client using the /v1/auth-servers/$AUTH_SERVER_ID/clients endpoint:

_response=$(curl -X POST "https://us.api.konghq.com/v1/auth-servers/$AUTH_SERVER_ID/clients" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $KONNECT_TOKEN"\
     -H "Content-Type: application/json" \
     --json '{
       "name": "orders-caller",
       "allow_all_scopes": true,
       "allow_scopes": [],
       "access_token_duration": 3600,
       "grant_types": [
         "client_credentials"
       ],
       "response_types": [
         "token"
       ],
       "redirect_uris": [],
       "login_uri": ""
     }')

Export the env variables:

export CLIENT_A_ID=$(echo "$_response" | jq -r ".id")
export CLIENT_A_SECRET=$(echo "$_response" | jq -r ".client_secret")

Create the reports-caller client:

_response=$(curl -X POST "https://us.api.konghq.com/v1/auth-servers/$AUTH_SERVER_ID/clients" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $KONNECT_TOKEN"\
     -H "Content-Type: application/json" \
     --json '{
       "name": "reports-caller",
       "allow_all_scopes": true,
       "allow_scopes": [],
       "access_token_duration": 3600,
       "grant_types": [
         "client_credentials"
       ],
       "response_types": [
         "token"
       ],
       "redirect_uris": [],
       "login_uri": ""
     }')

Export the env variables:

export CLIENT_B_ID=$(echo "$_response" | jq -r ".id")
export CLIENT_B_SECRET=$(echo "$_response" | jq -r ".client_secret")

Create the legacy-caller client. This caller’s Principal won’t carry any routing metadata, so you can use it to confirm the default backend:

_response=$(curl -X POST "https://us.api.konghq.com/v1/auth-servers/$AUTH_SERVER_ID/clients" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $KONNECT_TOKEN"\
     -H "Content-Type: application/json" \
     --json '{
       "name": "legacy-caller",
       "allow_all_scopes": true,
       "allow_scopes": [],
       "access_token_duration": 3600,
       "grant_types": [
         "client_credentials"
       ],
       "response_types": [
         "token"
       ],
       "redirect_uris": [],
       "login_uri": ""
     }')

Export the env variables:

export CLIENT_C_ID=$(echo "$_response" | jq -r ".id")
export CLIENT_C_SECRET=$(echo "$_response" | jq -r ".client_secret")

Create the Principals

Create one Principal per caller in the directory you created in the prerequisites. The metadata object is where the routing decision lives: backend_target holds the host:port address, and backend_scheme holds the protocol Kong Gateway uses to reach it.

Note: Metadata is inherited hierarchically. A Principal inherits its directory’s metadata and can override it, and a credential inherits the Principal’s metadata and can override it. You can set a backend_target on the directory to give every Principal a shared default.

Create the orders-caller Principal, routed to httpbin.konghq.com over HTTP:

PRINCIPAL_A_ID=$(curl -X POST "https://us.api.konghq.com/v2/directories/$DIRECTORY_ID/principals" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $KONNECT_TOKEN"\
     -H "Content-Type: application/json" \
     --json '{
       "display_name": "orders-caller",
       "description": "Caller routed to the orders backend",
       "metadata": {
         "backend_target": "httpbin.konghq.com:80",
         "backend_scheme": "http"
       }
     }' | jq -r ".id"
)

Create the reports-caller Principal, routed to httpbun.com over HTTPS:

PRINCIPAL_B_ID=$(curl -X POST "https://us.api.konghq.com/v2/directories/$DIRECTORY_ID/principals" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $KONNECT_TOKEN"\
     -H "Content-Type: application/json" \
     --json '{
       "display_name": "reports-caller",
       "description": "Caller routed to the reports backend",
       "metadata": {
         "backend_target": "httpbun.com:443",
         "backend_scheme": "https"
       }
     }' | jq -r ".id"
)

Create the legacy-caller Principal with no routing metadata:

PRINCIPAL_C_ID=$(curl -X POST "https://us.api.konghq.com/v2/directories/$DIRECTORY_ID/principals" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $KONNECT_TOKEN"\
     -H "Content-Type: application/json" \
     --json '{
       "display_name": "legacy-caller",
       "description": "Caller with no routing metadata, falls back to the default backend"
     }' | jq -r ".id"
)

Your Kong Identity authorization server issues the tokens. Add an auth_server_client identity to each Principal so Kong Identity can map the tokens it issues back to the right Principal.

Link orders-caller using the /v2/directories/$DIRECTORY_ID/principals/$PRINCIPAL_ID/identities endpoint:

curl -X POST "https://us.api.konghq.com/v2/directories/$DIRECTORY_ID/principals/$PRINCIPAL_A_ID/identities" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $KONNECT_TOKEN"\
     -H "Content-Type: application/json" \
     --json '{
       "type": "auth_server_client",
       "auth_server_id": "'$AUTH_SERVER_ID'",
       "client_id": "'$CLIENT_A_ID'"
     }'

Link reports-caller:

curl -X POST "https://us.api.konghq.com/v2/directories/$DIRECTORY_ID/principals/$PRINCIPAL_B_ID/identities" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $KONNECT_TOKEN"\
     -H "Content-Type: application/json" \
     --json '{
       "type": "auth_server_client",
       "auth_server_id": "'$AUTH_SERVER_ID'",
       "client_id": "'$CLIENT_B_ID'"
     }'

Link legacy-caller:

curl -X POST "https://us.api.konghq.com/v2/directories/$DIRECTORY_ID/principals/$PRINCIPAL_C_ID/identities" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $KONNECT_TOKEN"\
     -H "Content-Type: application/json" \
     --json '{
       "type": "auth_server_client",
       "auth_server_id": "'$AUTH_SERVER_ID'",
       "client_id": "'$CLIENT_C_ID'"
     }'

Get the directory name

To configure the OpenID Connect plugin, you need the name of the directory you created. Store it as DECK_DIRECTORY_NAME so decK can read it during sync:

DECK_DIRECTORY_NAME=$(curl -X GET "https://us.api.konghq.com/v2/directories" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $KONNECT_TOKEN" | jq -r ".data[0].name"
)

Export the directory name so decK can read it during sync:

export DECK_DIRECTORY_NAME

Generate a salt token

Starting with decK v1.59+, you need to set cache_tokens_salt to avoid regenerating session credentials during sync. Generate a salt token:

export DECK_TOKEN_SALT="$(openssl rand -base64 16)"

Enable the OpenID Connect plugin

Export the issuer URL to decK:

export DECK_ISSUER_URL=$ISSUER_URL

Configure the OpenID Connect plugin to verify bearer tokens issued by Kong Identity and hydrate the matching Principal:

echo '
_format_version: "3.0"
plugins:
  - name: openid-connect
    service: example-service
    config:
      issuer: "${{ env "DECK_ISSUER_URL" }}"
      auth_methods:
      - bearer
      audience:
      - http://myhttpbin.dev
      ssl_verify: true
      principals:
        enabled: true
        directory: "${{ env "DECK_DIRECTORY_NAME" }}"
      cache_tokens_salt: "${{ env "DECK_TOKEN_SALT" }}"
' | deck gateway apply -

In this configuration:

  • issuer: The Kong Identity authorization server issuer URL. The plugin discovers the JWKS endpoint from this URL and uses it to verify token signatures.
  • auth_methods: [bearer]: The plugin only accepts tokens in the Authorization: Bearer header.
  • audience: Must match the audience you set on the authorization server, so the plugin accepts the aud claim in the token.
  • principals.enabled: true: After verifying the token, the plugin looks up the matching Principal in the named directory and makes it available to later plugins through kong.client.principal.
  • principals.directory: The name of the Kong Identity directory holding your Principals.

Enable the Datakit plugin

Configure the Datakit plugin to read the Principal that the OpenID Connect plugin hydrated and use its metadata as the backend target:

echo '
_format_version: "3.0"
plugins:
  - name: datakit
    service: example-service
    config:
      nodes:
      - name: GET_PRINCIPAL
        type: property
        property: kong.client.principal
      - name: PICK_TARGET
        type: jq
        input: GET_PRINCIPAL
        jq: |
          {
            "target": (.metadata.backend_target // "httpbin.konghq.com:80"),
            "scheme": (.metadata.backend_scheme // "http")
          }
      - name: EXTRACT_TARGET
        type: jq
        input: PICK_TARGET
        jq: ".target"
      - name: SET_TARGET
        type: property
        property: kong.service.target
        input: EXTRACT_TARGET
      - name: EXTRACT_SCHEME
        type: jq
        input: PICK_TARGET
        jq: ".scheme"
      - name: SET_SCHEME
        type: property
        property: kong.service.request.scheme
        input: EXTRACT_SCHEME
      debug: true
' | deck gateway apply -

In this configuration:

  • GET_PRINCIPAL: Reads the kong.client.principal object that the OpenID Connect plugin populates. No input is connected because this is a read-only operation.
  • PICK_TARGET: Reads backend_target and backend_scheme out of the Principal’s metadata and returns both in one object. Returning both values from one node avoids reading the Principal twice. The // operator supplies a default backend for Principals that carry no routing metadata.
  • EXTRACT_TARGET: Extracts the .target field from the PICK_TARGET output.
  • SET_TARGET: Writes the host:port string to kong.service.target, overriding the backend for this request. This bypasses load balancing, health checks, and retries.
  • EXTRACT_SCHEME: Extracts the .scheme field from the PICK_TARGET output.
  • SET_SCHEME: Writes the scheme to kong.service.request.scheme. This is required when backends use different protocols, so Kong Gateway uses the correct scheme when connecting.
  • debug: true: Enables trace output for this tutorial. Remove it before using this configuration in production.

Validate the routing

To validate that routing via Datakit is working, fetch an access token for each client from the Kong Identity authorization server, then send it as a bearer token and verify that each caller reaches the correct backend.

In the following requests, you set the X-Datakit-Debug-Trace: true request header so that Datakit returns a JSON trace in the response body showing each node’s input and output.

  1. Fetch a token as orders-caller:

    export TOKEN_A=$(curl -s -X POST "$ISSUER_URL/oauth/token" \
      -H "Content-Type: application/x-www-form-urlencoded" \
      -d "grant_type=client_credentials" \
      -d "client_id=$CLIENT_A_ID" \
      -d "client_secret=$CLIENT_A_SECRET" | jq -r .access_token)

    Send a request as orders-caller:

    curl -si http://localhost:8000/anything \
      -H "Authorization: Bearer $TOKEN_A" \
      -H "X-Datakit-Debug-Trace: true"

    The response comes from httpbin.konghq.com. In the response body, find the complete event for each node and check:

    • GET_PRINCIPAL: value.value.display_name is orders-caller.
    • PICK_TARGET: value.value is {"target":"httpbin.konghq.com:80","scheme":"http"}.
    • EXTRACT_TARGET: value.value is httpbin.konghq.com:80.
    • SET_TARGET: value.value is httpbin.konghq.com:80.
    • EXTRACT_SCHEME: value.value is http.
    • SET_SCHEME: value.value is http.
  2. Fetch a token as reports-caller:

    export TOKEN_B=$(curl -s -X POST "$ISSUER_URL/oauth/token" \
      -H "Content-Type: application/x-www-form-urlencoded" \
      -d "grant_type=client_credentials" \
      -d "client_id=$CLIENT_B_ID" \
      -d "client_secret=$CLIENT_B_SECRET" | jq -r .access_token)

    Send a request as reports-caller:

    curl -si http://localhost:8000/anything \
      -H "Authorization: Bearer $TOKEN_B" \
      -H "X-Datakit-Debug-Trace: true"

    The response comes from httpbun.com, confirming the request was routed to a different backend. EXTRACT_TARGET and SET_TARGET should show httpbun.com:443, and EXTRACT_SCHEME and SET_SCHEME should show https.

  3. Fetch a token as legacy-caller to confirm the fallback:

    export TOKEN_C=$(curl -s -X POST "$ISSUER_URL/oauth/token" \
      -H "Content-Type: application/x-www-form-urlencoded" \
      -d "grant_type=client_credentials" \
      -d "client_id=$CLIENT_C_ID" \
      -d "client_secret=$CLIENT_C_SECRET" | jq -r .access_token)

    Send a request as legacy-caller:

    curl -si http://localhost:8000/anything \
      -H "Authorization: Bearer $TOKEN_C" \
      -H "X-Datakit-Debug-Trace: true"

    GET_PRINCIPAL should show value.value.display_name as legacy-caller with an empty metadata object, and EXTRACT_TARGET and SET_TARGET should resolve to httpbin.konghq.com:80 because the Principal carries no backend_target.

  4. Send a request with no token. The OpenID Connect plugin rejects it before Datakit runs:

curl -i $KONNECT_PROXY_URL/anything 

This request returns a 401 error with the message Unauthorized.

Change a caller’s backend without a decK sync

Because the routing map lives in Kong Identity, you can move a caller to a different backend by updating its Principal metadata.

Move orders-caller to httpbun.com:

curl -X PATCH "https://us.api.konghq.com/v2/directories/$DIRECTORY_ID/principals/$PRINCIPAL_A_ID" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $KONNECT_TOKEN"\
     -H "Content-Type: application/json" \
     --json '{
       "metadata": {
         "backend_target": "httpbun.com:443",
         "backend_scheme": "https"
       }
     }'

Send a request as orders-caller again. Once the OpenID Connect plugin’s token cache expires, the response comes from httpbun.com with no change to your gateway configuration:

curl -si http://localhost:8000/anything \
  -H "Authorization: Bearer $TOKEN_A" \
  -H "X-Datakit-Debug-Trace: true"

Cleanup

If you created a new control plane and want to conserve your free trial credits or avoid unnecessary charges, delete the new control plane used in this tutorial.

curl -Ls https://get.konghq.com/quickstart | bash -s -- -d

FAQs

No, the secret is only shared once when the client is created. Store it securely.

By default, the OpenID Connect plugin returns a 401 when it verifies a token but can’t match it to a Principal in the directory. Set principals.error_on_miss to false if you want the request to continue without an authenticated Principal. If you do that, handle a null kong.client.principal in your Datakit configuration.

Help us make these docs great!

Kong Developer docs are open source. If you find these useful and want to make them better, contribute today!