Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ related_resources:
url: /gateway/authentication/
- text: Token exchange in OIDC
url: /plugins/openid-connect/#token-exchange
- text: Configure multi-IdP with a trusted issuer registry
url: /how-to/configure-oidc-with-multi-idp/
- text: OpenID Connect tutorials
url: /how-to/?query=openid-connect

Expand Down
173 changes: 173 additions & 0 deletions app/_how-tos/gateway/configure-oidc-with-multi-idp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
---
title: Configure OpenID Connect with multiple IdPs using a trusted issuer registry
permalink: /how-to/configure-oidc-with-multi-idp/
content_type: how_to
description: Learn how to configure the OpenID Connect plugin to validate tokens from multiple identity providers using a trusted issuer registry.

related_resources:
- text: OpenID Connect in {{site.base_gateway}}
url: /gateway/openid-connect/
- text: Authentication in {{site.base_gateway}}
url: /gateway/authentication/
- text: Multi-IdP token validation at the gateway layer
url: /plugins/openid-connect/multi-idp/
- text: Configure OpenID Connect with token exchange
url: /how-to/configure-oidc-with-token-exchange/
- text: OpenID Connect tutorials
url: /how-to/?query=openid-connect

plugins:
- openid-connect

entities:
- route
- service
- plugin

products:
- gateway

works_on:
- on-prem
- konnect

tools:
- deck

prereqs:
entities:
services:
- example-service
routes:
- example-route
inline:
- title: Set up Keycloak with two realms
include_content: prereqs/auth/oidc/keycloak-multi-idp
icon_url: /assets/icons/keycloak.svg

tags:
- authentication
- openid-connect
search_aliases:
- oidc
- multi-idp
- multiple identity providers

tldr:
q: How do I configure {{site.base_gateway}} to accept tokens from multiple identity providers on the same route?
a: |
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`.
{{site.base_gateway}} validates incoming tokens against the matching JWKS and forwards them to the upstream without transformation.
Comment thread
lena-larionova marked this conversation as resolved.

cleanup:
inline:
- title: Clean up Konnect environment
include_content: cleanup/platform/konnect
icon_url: /assets/icons/gateway.svg
- title: Destroy the {{site.base_gateway}} container
include_content: cleanup/products/gateway
icon_url: /assets/icons/gateway.svg

automated_tests: false
---

## Generate salt token

{% include how-tos/steps/deck-salt-token.md %}

## Enable the OpenID Connect plugin for multiple IdPs

Using the Keycloak configuration from the [prerequisites](#prerequisites), configure the OpenID Connect plugin on `example-route` to accept tokens from both realms:

{% entity_examples %}
entities:
plugins:
- name: openid-connect
route: example-route
config:
issuer: ${realm-a-issuer}
using_pseudo_issuer: true
jwks_endpoint: ${realm-a-jwks}
auth_methods:
- bearer
extra_jwks_uris:
- ${realm-b-jwks}
issuers_allowed:
- ${realm-a-issuer}
- ${realm-b-issuer}
verify_signature: true
verify_claims: false
cache_tokens_salt: ${salt-token}
variables:
realm-a-issuer:
value: $REALM_A_ISSUER
realm-a-jwks:
value: $REALM_A_JWKS
realm-b-jwks:
value: $REALM_B_JWKS
realm-b-issuer:
value: $REALM_B_ISSUER
{% endentity_examples %}

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 {{site.base_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 {{site.base_gateway}} uses to fetch signing keys for `realm-a`. Uses the `keycloak` container name, which is reachable from {{site.base_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`:

```sh
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 {{site.base_gateway}}:

{% validation request-check %}
url: /anything
method: GET
status_code: 200
display_headers: true
headers:
- "Authorization: Bearer $TOKEN_A"
{% endvalidation %}

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`:

```sh
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 {{site.base_gateway}}:

{% validation request-check %}
url: /anything
method: GET
status_code: 200
display_headers: true
headers:
- "Authorization: Bearer $TOKEN_B"
{% endvalidation %}

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 {{site.base_gateway}} accepted the token from the second IdP and forwarded it unchanged.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!--vale off-->
{% mermaid %}
sequenceDiagram
participant C as Client
participant K as API Gateway<br>with OIDC plugin
participant IdPA as IdP A<br>(primary issuer)
participant IdPB as IdP B<br>(via extra_jwks_uris)
participant U as Upstream<br>(backend service)

C->>K: Request with bearer token
activate K
K->>K: Extract iss claim from token
alt If iss matches IdP A
K->>IdPA: Fetch JWKS (if not cached)
IdPA-->>K: Public keys
else If iss matches IdP B
K->>IdPB: Fetch JWKS (if not cached)
IdPB-->>K: Public keys
end
K->>K: Verify signature
K->>K: Check iss against issuers_allowed
K->>U: Proxy request with original token
activate U
U-->>K: Response
deactivate U
K-->>C: Response
deactivate K
{% endmermaid %}
<!--vale on-->
130 changes: 130 additions & 0 deletions app/_includes/prereqs/auth/oidc/keycloak-multi-idp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
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](https://www.keycloak.org/guides) (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 {{site.base_gateway}} so that the OIDC plugin can reach it:

```sh
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 {{site.base_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.

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

```sh
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 {{site.base_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`.

1. Open the Keycloak admin console.

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

1. 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**.
1. Configure the client:

<!--vale off-->
{% table %}
columns:
- title: Section
key: section
- title: Settings
key: settings
rows:
- section: "**General settings**"
settings: |
* Client type: **OpenID Connect**
* Client ID: `client-a`
- section: "**Capability config**"
settings: |
* Toggle **Client authentication** to **on**
* Select the **Service accounts roles** checkbox.
{% endtable %}
<!--vale on-->

Find the credentials for `client-a`:

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

```sh
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**.
1. Click **Create realm**.
1. Set **Realm name** to `realm-b`.
1. Click **Create**.

Create a client for `realm-b`:

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

<!--vale off-->
{% table %}
columns:
- title: Section
key: section
- title: Settings
key: settings
rows:
- section: "**General settings**"
settings: |
* Client type: **OpenID Connect**
* Client ID: `client-b`
- section: "**Capability config**"
settings: |
* Toggle **Client authentication** to **on**
* Select the **Service accounts roles** checkbox.
{% endtable %}
<!--vale on-->

Find the credentials for `client-b`:

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

```sh
export DECK_CLIENT_B_SECRET='YOUR-CLIENT-SECRET'
```
4 changes: 3 additions & 1 deletion app/_kong_plugins/openid-connect/examples/extra-jwks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ extended_description: |

This example shows how to configure two different `extra_jwks_uris` to support token validation for two different IdPs.

For a complete example with Keycloak as the IdP, see the tutorial for [configuring OpenID Connect with multiple IdPs using a trusted issuer registry](/how-to/configure-oidc-with-multi-idp/).

{:.info}
> **Note**: `extra_jwks_uris` adds multiple discovery endpoints, but the plugin will still look at the `jwks_uri` returned by the internal discovery mechanism first, introducing potential latency.
If you want to override the default discovery JWKS endpoint instead of providing multiple fallback options, see the [Use a custom JWKS endpoint for discovery example](/plugins/openid-connect/examples/override-jwks-endpoint/).
If you want to override the default discovery JWKS endpoint instead of providing multiple fallback options, see the [Use a custom JWKS endpoint for discovery example](/plugins/openid-connect/examples/override-jwks-endpoint/).
weight: 698

requirements:
Expand Down
22 changes: 20 additions & 2 deletions app/_kong_plugins/openid-connect/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,15 @@ faqs:
a: |
Yes, but since the OIDC plugin only accepts one issuer URL, this requires some extra configuration.

You can verify tokens issued by multiple IdP using the [`extra_jwks_uris`](/plugins/openid-connect/reference/#schema--config-extra-jwks-uris) configuration option, with the following considerations:
You can verify tokens issued by multiple IdP using the [`extra_jwks_uris`](/plugins/openid-connect/reference/#schema--config-extra-jwks-uris) configuration option with the following considerations:

* Since the plugin only accepts a single issuer, any `iss` claim verification will fail for tokens that come from a different IdP than the one that was used in the issuer configuration option. Add all issuers as they appear in the `iss` claims of your tokens to the [`config.issuers_allowed`](/plugins/openid-connect/reference/#schema--config-issuers-allowed) setting.
* If you make any changes to the `extra_jwks_uris` value, you have to clear the second level DB cache for the change to become effective.
See [Delete a Discovery Cache Object](/plugins/openid-connect/api/#/operations/deleteDiscoveryCache).

See the [Extra JWKs](/plugins/openid-connect/examples/extra-jwks/) configuration example for more detail.
You can also use [token exchange](#token-exchange), which allows exchanging an existing security token for a new one.

See the [OIDC multi-IdP](/plugins/openid-connect/multi-idp/) reference for more detail.
- q: How do I enable the Proof Key for Code Exchange (PKCE) extension to the authorization code flow in the OIDC plugin?
a: |
The OIDC plugin supports PKCE out of the box, so you don't need to configure anything.
Expand Down Expand Up @@ -562,6 +564,22 @@ To enable DPoP for OpenID Connect:

See the [DPoP configuration example](/plugins/openid-connect/examples/dpop/) for more detail.

## Multi-IdP support

If your APIs serve clients that authenticate with different identity providers, the OIDC plugin can validate tokens from multiple issuers at the gateway layer, so backends don't need per-IdP logic.

You can implement this in one of the following ways:

* **Trusted issuers registry**: Configure the OIDC plugin with a list of trusted issuers and their JWKS endpoints using [`config.issuers_allowed`](/plugins/openid-connect/reference/#schema--config-issuers-allowed) and [`config.extra_jwks_uris`](/plugins/openid-connect/reference/#schema--config-extra-jwks-uris).
{{site.base_gateway}} validates incoming tokens against the appropriate public keys and forwards them to the backend as-is.
This works best when token formats are consistent across IdPs.

* **Token exchange** {% new_in 3.14 %}: Configure the OIDC plugin to swap incoming tokens for a canonical token from one trusted issuer using [`config.token_exchange`](/plugins/openid-connect/reference/#schema--config-token-exchange).
The backend always receives tokens from a single issuer regardless of which IdP the client used.
This works best when backends must trust one issuer, or when you need to normalize scopes and claims across IdPs.

For a detailed comparison, configuration parameters, and examples, see [Multi-IdP token validation at the gateway layer](/plugins/openid-connect/multi-idp/).

## Token exchange {% new_in 3.14 %}

The [OAuth 2.0 Token Exchange](https://oauth.net/2/token-exchange/) (RFC 8693) is an extension to the OAuth 2.0 framework that allows exchanging an existing security token for a new one.
Expand Down
Loading
Loading