Configure OpenID Connect with multiple IdPs using a trusted issuer registry

TL;DR

Configure the OpenID Connect plugin with extra_jwks_uris listing each IdP’s JWKS endpoint and issuers_allowed listing each IdP’s issuer URL. Set verify_claims to false so that the iss claim is checked against issuers_allowed rather than requiring it to match config.issuer. Kong Gateway validates incoming tokens against the matching JWKS and forwards them to the upstream without transformation.

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 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.

This tutorial requires Kong Gateway Enterprise. If you don’t have Kong Gateway set up yet, you can use the quickstart script with an enterprise license to get an instance of Kong Gateway running almost instantly.

  1. Export your license to an environment variable:

     export KONG_LICENSE_DATA='LICENSE-CONTENTS-GO-HERE'
  2. Run the quickstart script:

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

    Once Kong Gateway is ready, you will see the following message:

     Kong Gateway Ready

To complete this tutorial, install decK. We recommend keeping decK up to date with the latest version (1.62.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.

This tutorial requires two identity providers (IdPs). If you don’t have them, you can simulate two separate IdPs using two Keycloak realms. The steps will be similar with other standard identity providers.

Install and run Keycloak

  1. Install Keycloak (version 26 or later) on your platform.

    For example, you can use the Keycloak Docker image. The following command attaches Keycloak to the same network as Kong Gateway so that the OIDC plugin can reach it:

     docker run -p 127.0.0.1:8080:8080 \
       --name keycloak \
       --network kong-quickstart-net \
       -e KC_BOOTSTRAP_ADMIN_USERNAME=admin \
       -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
       -e KC_HOSTNAME=http://localhost:8080 \
       quay.io/keycloak/keycloak start-dev

    The parameter KC_HOSTNAME=http://localhost:8080 ensures Keycloak always uses localhost:8080 as its token issuer regardless of which URL it’s accessed through. This is required because Kong Gateway runs inside Docker and accesses Keycloak via the container name keycloak:8080, but the iss claim in issued tokens must use localhost:8080 for the plugin to recognize them.

  2. Export your environment variables. For example, using Docker and the master and realm-b realms:

    export DECK_REALM_A_ISSUER='http://localhost:8080/realms/master'
    export DECK_REALM_B_ISSUER='http://localhost:8080/realms/realm-b'
    export DECK_REALM_A_JWKS='http://keycloak:8080/realms/master/protocol/openid-connect/certs'
    export DECK_REALM_B_JWKS='http://keycloak:8080/realms/realm-b/protocol/openid-connect/certs'
    export KEYCLOAK_HOST='localhost'

    Because we’re using Docker for this demo, we must configure a few networking parameters:

    • DECK_REALM_A_ISSUER and DECK_REALM_B_ISSUER use localhost because that’s how you access Keycloak from your machine.
    • DECK_REALM_A_JWKS and DECK_REALM_B_JWKS use the container name keycloak because Kong Gateway runs inside Docker and reaches Keycloak over the shared kong-quickstart-net network.

    In your own setup, especially running outside of a container, you may not need DECK_REALM_A_JWKS and DECK_REALM_B_JWKS.

  3. Open the Keycloak admin console.

    The default URL is http://localhost:8080/admin/master/console/.

  4. Log in with the credentials you defined when you launched Keycloak. For this example, the credentials are username admin and password admin.

Configure the first IdP

The master realm acts as the first identity provider. Create a client for realm-a:

  1. In the sidebar, open Clients, then click Create client.
  2. Configure the client:

Section

Settings

General settings
  • Client type: OpenID Connect
  • Client ID: client-a
Capability config
  • Toggle Client authentication to on
  • Select the Service accounts roles checkbox.

Find the credentials for client-a:

  1. In the sidebar, open Clients, and select client-a.
  2. Click the Credentials tab.
  3. Set Client Authenticator to Client ID and Secret.
  4. Copy the Client Secret.
  5. Export the client secret to an environment variable:

    export DECK_CLIENT_A_SECRET='YOUR-CLIENT-SECRET'

Configure the second IdP

Create a second Keycloak realm to simulate a second identity provider:

  1. In the sidebar click Manage realms.
  2. Click Create realm.
  3. Set Realm name to realm-b.
  4. Click Create.

Create a client for realm-b:

  1. Make sure you’re in the realm-b realm (check the top-left dropdown).
  2. In the sidebar, open Clients, then click Create client.
  3. Configure the client:

Section

Settings

General settings
  • Client type: OpenID Connect
  • Client ID: client-b
Capability config
  • Toggle Client authentication to on
  • Select the Service accounts roles checkbox.

Find the credentials for client-b:

  1. In the sidebar, open Clients, and select client-b.
  2. Open the Credentials tab.
  3. Set Client Authenticator to Client ID and Secret.
  4. Copy the Client Secret.
  5. Export the client secret to an environment variable:

    export DECK_CLIENT_B_SECRET='YOUR-CLIENT-SECRET'

Generate 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)"
export DECK_TOKEN_SALT="$(openssl rand -base64 16)"

Enable the OpenID Connect plugin for multiple IdPs

Using the Keycloak configuration from the prerequisites, configure the OpenID Connect plugin on example-route to accept tokens from both realms:

echo '
_format_version: "3.0"
plugins:
  - name: openid-connect
    route: example-route
    config:
      issuer: "${{ env "DECK_REALM_A_ISSUER" }}"
      using_pseudo_issuer: true
      jwks_endpoint: "${{ env "DECK_REALM_A_JWKS" }}"
      auth_methods:
      - bearer
      extra_jwks_uris:
      - "${{ env "DECK_REALM_B_JWKS" }}"
      issuers_allowed:
      - "${{ env "DECK_REALM_A_ISSUER" }}"
      - "${{ env "DECK_REALM_B_ISSUER" }}"
      verify_signature: true
      verify_claims: false
      cache_tokens_salt: "${salt-token}"
' | deck gateway apply -

Auth configuration:

  • issuer: The primary IdP URL, used for discovery and as the canonical issuer reference.
  • using_pseudo_issuer: Disables OIDC discovery from the issuer URL. Required here because Kong Gateway runs inside Docker and can’t reach localhost:8080 directly. The issuer value is still used to match the iss claim in tokens from realm-a.
  • jwks_endpoint: Explicit JWKS endpoint that Kong Gateway uses to fetch signing keys for realm-a. Uses the keycloak container name, which is reachable from Kong Gateway over the shared Docker network.
  • extra_jwks_uris: The JWKS endpoint for realm-b. The plugin checks the primary JWKS first, then falls back to this list.
  • issuers_allowed: Explicit allowlist of accepted issuers. Tokens whose iss claim doesn’t match one of these values are rejected.
  • verify_claims: Set to false so that the iss claim is checked against issuers_allowed instead of requiring it to equal config.issuer. Without this, tokens from realm-b would fail claim verification.

Validate the flow

Token from realm-a

Get a client credentials token from realm-a:

TOKEN_A=$(curl -s -X POST \
  http://$KEYCLOAK_HOST:8080/realms/master/protocol/openid-connect/token \
  -d "grant_type=client_credentials" \
  -d "client_id=client-a" \
  -d "client_secret=$DECK_CLIENT_A_SECRET" | jq -r .access_token) && echo $TOKEN_A

Send the token to Kong Gateway:

curl -i -X GET "$KONNECT_PROXY_URL/anything" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $TOKEN_A"
curl -i -X GET "http://localhost:8000/anything" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $TOKEN_A"

You should see a 200 response. When you decode the token in the forwarded Authorization header, the iss claim should be http://localhost:8080/realms/master.

Token from realm-b

Get a client credentials token from realm-b:

TOKEN_B=$(curl -s -X POST \
  http://$KEYCLOAK_HOST:8080/realms/realm-b/protocol/openid-connect/token \
  -d "grant_type=client_credentials" \
  -d "client_id=client-b" \
  -d "client_secret=$DECK_CLIENT_B_SECRET" | jq -r .access_token) && echo $TOKEN_B

Send the token to Kong Gateway:

curl -i -X GET "$KONNECT_PROXY_URL/anything" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $TOKEN_B"
curl -i -X GET "http://localhost:8000/anything" \
     --no-progress-meter --fail-with-body  \
     -H "Authorization: Bearer $TOKEN_B"

You should see a 200 response. When you decode the token in the forwarded Authorization header, the iss claim should be http://localhost:8080/realms/realm-b, confirming that Kong Gateway accepted the token from the second IdP and forwarded it unchanged.

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

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!