From a12108b4df35c74efb3b8ab7732e0af490a14a84 Mon Sep 17 00:00:00 2001 From: lena-larionova Date: Thu, 11 Jun 2026 11:11:26 -0700 Subject: [PATCH 1/4] multi-idp reference and guide for extra_jwks_urls --- .../gateway/configure-oidc-with-multi-idp.md | 171 +++++++++++++++ .../diagrams/multi-idp-trusted-issuers.md | 28 +++ .../prereqs/auth/oidc/keycloak-multi-idp.md | 130 ++++++++++++ app/_kong_plugins/openid-connect/index.md | 24 ++- app/gateway/plugins/oidc/multi-idp.md | 197 ++++++++++++++++++ 5 files changed, 546 insertions(+), 4 deletions(-) create mode 100644 app/_how-tos/gateway/configure-oidc-with-multi-idp.md create mode 100644 app/_includes/plugins/oidc/diagrams/multi-idp-trusted-issuers.md create mode 100644 app/_includes/prereqs/auth/oidc/keycloak-multi-idp.md create mode 100644 app/gateway/plugins/oidc/multi-idp.md diff --git a/app/_how-tos/gateway/configure-oidc-with-multi-idp.md b/app/_how-tos/gateway/configure-oidc-with-multi-idp.md new file mode 100644 index 0000000000..bafa9db260 --- /dev/null +++ b/app/_how-tos/gateway/configure-oidc-with-multi-idp.md @@ -0,0 +1,171 @@ +--- +title: Configure OpenID Connect with multiple IdPs using a trusted issuers 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 issuers 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: 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. + +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. diff --git a/app/_includes/plugins/oidc/diagrams/multi-idp-trusted-issuers.md b/app/_includes/plugins/oidc/diagrams/multi-idp-trusted-issuers.md new file mode 100644 index 0000000000..5f5ba19a41 --- /dev/null +++ b/app/_includes/plugins/oidc/diagrams/multi-idp-trusted-issuers.md @@ -0,0 +1,28 @@ +{% mermaid %} +sequenceDiagram + participant C as Client + participant K as API Gateway
with OIDC plugin + participant IdPA as IdP A
(primary issuer) + participant IdPB as IdP B
(via extra_jwks_uris) + participant U as Upstream
(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->>K: Verify exp, nbf + K->>U: Proxy request with original token + activate U + U-->>K: Response + deactivate U + K-->>C: Response + deactivate K +{% endmermaid %} diff --git a/app/_includes/prereqs/auth/oidc/keycloak-multi-idp.md b/app/_includes/prereqs/auth/oidc/keycloak-multi-idp.md new file mode 100644 index 0000000000..af0fb7cffb --- /dev/null +++ b/app/_includes/prereqs/auth/oidc/keycloak-multi-idp.md @@ -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 have to 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: + + +{% 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** + * Make sure that **Service accounts roles** is checked. +{% endtable %} + + +Find the credentials for `client-a`: + +1. In the sidebar, open **Clients**, and select `client-a`. +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_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: + + +{% 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** + * Make sure that **Service accounts roles** is checked. +{% endtable %} + + +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' + ``` diff --git a/app/_kong_plugins/openid-connect/index.md b/app/_kong_plugins/openid-connect/index.md index ccf287b0ff..c17153dafd 100644 --- a/app/_kong_plugins/openid-connect/index.md +++ b/app/_kong_plugins/openid-connect/index.md @@ -562,7 +562,23 @@ To enable DPoP for OpenID Connect: See the [DPoP configuration example](/plugins/openid-connect/examples/dpop/) for more detail. -## Token exchange {% new_in 3.14 %} +## 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`. +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. The RFC defines a protocol approach to support scenarios where a client can exchange a token for a new token by interacting with the authorization server. @@ -571,7 +587,7 @@ This is particularly useful in complex environments like microservices or cross- {:.info} > **Note**: The OpenID Connect plugin only supports exchanging access tokens. -### Why use token exchange? +#### Why use token exchange? Token exchange can be used in several critical use cases: @@ -586,7 +602,7 @@ For example, a frontend service needs to trade its token for a new token with sp > Because token exchange allows for the creation of new tokens, trust models are vital. The trust model must strictly define which clients are allowed to exchange tokens and which scopes they are permitted to elevate or downgrade to prevent security flaws like privilege escalations. -### How token exchange works +#### How token exchange works In a typical [OAuth flow](#kong-oauth-token-authentication-flow), a token is obtained to access a resource. However, in a token exchange, a client already has a token (the "subject token"). @@ -613,7 +629,7 @@ Set up token exchange: * [Example: Token transformation](/plugins/openid-connect/examples/token-exchange-transformation/) * [How-to: Configure OIDC with token exchange](/how-to/configure-oidc-with-token-exchange/) -#### Key terms +##### Key terms The token exchange flow uses the following terms: diff --git a/app/gateway/plugins/oidc/multi-idp.md b/app/gateway/plugins/oidc/multi-idp.md new file mode 100644 index 0000000000..36f8256bcb --- /dev/null +++ b/app/gateway/plugins/oidc/multi-idp.md @@ -0,0 +1,197 @@ +--- +title: Multi-IdP token validation at the Gateway layer + +description: "Configure the OpenID Connect plugin to validate tokens from multiple identity providers using a trusted issuer registry or token exchange." +content_type: reference +layout: reference +permalink: /plugins/openid-connect/multi-idp/ + +products: + - gateway + +breadcrumbs: + - /plugins/ + - /plugins/openid-connect/ + + +works_on: + - on-prem + - konnect + +related_resources: + - text: OpenID Connect in {{site.base_gateway}} + url: /gateway/openid-connect/ + - text: OpenID Connect plugin reference + url: /plugins/openid-connect/ + - text: "How-to: Configure OpenID Connect with multiple IdPs" + url: /how-to/configure-oidc-with-multi-idp/ + - text: "How-to: Configure OpenID Connect with token exchange using Keycloak" + url: /how-to/configure-oidc-with-token-exchange/ +--- + +If your APIs serve clients from multiple identity providers (IdPs), for example employees using Okta, B2B partners using Azure AD, or legacy systems on an in-house IdP, the [OpenID Connect (OIDC) plugin](/plugins/openid-connect/) can act as a federated authentication broker at the gateway layer. +Backends don't need per-IdP validation logic. +{{site.base_gateway}} centralizes auth policy and forwards only the verified identity context upstream. + +The OIDC plugin supports two approaches for multi-IdP authentication, both using [JWT access token (bearer) auth](/plugins/openid-connect/#jwt-access-token-authentication-flow). +Clients authenticate against their respective IdPs and present the resulting bearer token to {{site.base_gateway}}: + +{% table %} +columns: + - title: "" + key: label + - title: "[Trusted issuers registry](#option-1-trusted-issuers-registry)" + key: option1 + - title: "[Token exchange](#option-2-token-exchange)" + key: option2 +rows: + - label: "How it works" + option1: "Validates tokens from multiple issuers against their JWKS endpoints. Backends receive the original, unmodified token." + option2: "Exchanges incoming tokens for a canonical token from one trusted target issuer. Backends only ever see tokens from that issuer." + - label: "When to use" + option1: "Token formats are consistent across IdPs. Backends can accept tokens from different issuers." + option2: "Backends must trust a single issuer. Token formats differ across IdPs. You need downscoping, normalization, or cross-domain federation." + - label: "IdP requirements" + option1: "No special grant needed." + option2: "IdPs must support [RFC 8693](https://www.rfc-editor.org/rfc/rfc8693) token exchange." + - label: "Min version" + option1: "Any" + option2: "3.14" + - label: "Key config parameters" + option1: "`config.issuers_allowed`, `config.extra_jwks_uris`" + option2: "`config.token_exchange.subject_token_issuers`" +{% endtable %} + +## Option 1: Trusted issuers registry + +The plugin inspects the `iss` claim of an incoming bearer token, looks up the matching JWKS endpoint from the configured list, validates the token signature and standard claims, then forwards the verified request upstream. +No token transformation occurs. + +{% include_cached /plugins/oidc/diagrams/multi-idp-trusted-issuers.md %} + +Configure the following OIDC plugin settings: + +* [`config.issuers_allowed`](/plugins/openid-connect/reference/#schema--config-issuers-allowed): Allowlist of issuer URLs the plugin will accept. +Add every IdP's issuer URL here, exactly as it appears in the `iss` claim of their tokens. +* [`config.extra_jwks_uris`](/plugins/openid-connect/reference/#schema--config-extra-jwks-uris): Additional JWKS endpoints for each IdP beyond the primary `config.issuer`. +The plugin checks the primary discovery JWKS first, then falls back to these. + +This approach works best when tokens issued by each IdP follow the same claim naming conventions. + +{:.info} +> The plugin uses `config.issuer` for discovery and to identify the primary issuer. +> Tokens from other IdPs will fail `iss` claim verification unless you set [`config.verify_claims`](/plugins/openid-connect/reference/#schema--config-verify-claims) to `false` and control allowed issuers via `config.issuers_allowed` instead. + +If you update `config.extra_jwks_uris` after the plugin is already configured, [clear the discovery cache](/plugins/openid-connect/api/#/operations/deleteDiscoveryCache) for the change to take effect. + +### Configuration example + +The following example configures the OIDC plugin to accept tokens from two identity providers. +The first IdP is the primary `config.issuer`, while the second is added via `config.extra_jwks_uris`: + +```yaml +config: + issuer: https://idp-a.example.com + auth_methods: + - bearer + extra_jwks_uris: + - https://idp-b.example.com/oauth2/v1/keys + issuers_allowed: + - https://idp-a.example.com + - https://idp-b.example.com + verify_signature: true + verify_claims: false +``` + +In this example, a client authenticated with `idp-a` presents a bearer token. +{{site.base_gateway}} validates it against `idp-a`'s JWKS and checks the issuer against `config.issuers_allowed`. +The same flow applies to a client from `idp-b`. +Both reach the upstream service without any token transformation. + +For more detail and a complete walkthrough: +* [Plugin example: Token validation for multiple IdPs](/plugins/openid-connect/examples/extra-jwks/) +* [How-to: Configure OpenID Connect with multiple IdPs](/how-to/configure-oidc-with-multi-idp/) + +## Option 2: Token exchange {% new_in 3.14 %} + +Token exchange is a standard, protocol-driven way to swap an incoming security token for a new one. +Using JWT access token authentication, the plugin validates the incoming bearer token (the "subject token"), then uses its own client credentials to exchange it with the target issuer per [RFC 8693](https://www.rfc-editor.org/rfc/rfc8693). +The upstream service receives the exchanged token from the target issuer, regardless of which IdP the client originally authenticated with. + +{% include_cached /plugins/oidc/diagrams/token-exchange.md %} + +This approach also unlocks use cases beyond multi-IdP: + +* Downscoping: Exchange a high-privilege token for one with fewer scopes before forwarding to a less-trusted upstream. +* Cross-domain federation: Exchange tokens when clients use one IdP but your APIs are protected by another. +* Token translation: Convert tokens with different claim structures into a consistent internal format that your microservices understand. +* On-behalf flows: Set up delegation and impersonation, where one client acts on behalf of another. + +Key configuration parameters: + +* [`config.token_exchange.subject_token_issuers`](/plugins/openid-connect/reference/#schema--config-token-exchange-subject-token-issuers): Explicit list of trusted input issuers. +The plugin only exchanges tokens whose `iss` claim matches an entry here. +* [`config.token_exchange.conditions`](/plugins/openid-connect/reference/#schema--config-token-exchange-conditions): Optional per-issuer rules that control when to trigger the exchange. +If the subject token issuer and the target issuer (the one configured in `config.issuer`) are different, exchange always triggers. +If they match, conditions determine whether to exchange. +* [`config.token_exchange.request`](/plugins/openid-connect/reference/#schema--config-token-exchange-request): The scopes and audience to request in the exchanged token. + +With token exchange, trust is strictly enforced on both sides. +{{site.base_gateway}} only exchanges tokens whose issuer is explicitly listed in `subject_token_issuers`. +Each IdP in the list must also authorize {{site.base_gateway}} as a trusted client eligible for the token exchange grant. + +### Configuration example + +The following example configures {{site.base_gateway}} to act as `kong-client` at the target issuer (`idp-a`) and exchange tokens issued by `idp-b`: + +```yaml +config: + issuer: https://idp-a.example.com + client_id: + - kong-client + client_secret: + - ${kong-client-secret} + client_auth: + - client_secret_post + auth_methods: + - bearer + token_exchange: + subject_token_issuers: + - issuer: https://idp-b.example.com +``` + +When a client from `idp-b` presents a bearer token, {{site.base_gateway}} validates it, then exchanges it with `idp-a` to produce a token the upstream service trusts. +Tokens already issued by `idp-a` are validated as-is unless conditions require an exchange. + +For more detail and complete walkthroughs: +* [Plugin example: Token exchange for cross-domain security](/plugins/openid-connect/examples/token-exchange-cross-domain/) +* [Plugin example: Token transformation](/plugins/openid-connect/examples/token-exchange-transformation/) +* [How-to: Configure OpenID Connect with token exchange using Keycloak](/how-to/configure-oidc-with-token-exchange/) +* [Token exchange reference](/plugins/openid-connect/#token-exchange) + +## Troubleshooting + +The following errors can appear in the {{site.base_gateway}} error log whether you're using one issuer or multiple. +The fix differs depending on which approach you're using. +For general debugging steps, see [Debugging the OIDC plugin](/plugins/openid-connect/#debugging-the-oidc-plugin). + +{% table %} +columns: + - title: Error + key: error + - title: Log message + key: log + - title: Likely cause + key: cause +rows: + - error: Expired token + log: "`invalid exp claim () was specified for access token`" + cause: | + The token's `exp` claim is in the past. Get a fresh token from the IdP. + - error: Signature verification failure + log: "`invalid signature (pkey:verify: ...)`" + cause: | + The signing key for the token's issuer isn't available to the plugin. + * If using a trusted issuer registry, check that the issuer's JWKS endpoint is listed in `config.extra_jwks_uris` and that the [discovery cache has been cleared](/plugins/openid-connect/api/#/operations/deleteDiscoveryCache) after any config change. + * If using token exchange, confirm that `config.token_exchange.subject_token_issuers` includes the token's `iss` value. +{% endtable %} From fc59584724b96ad12fad92c74aad9678652065d4 Mon Sep 17 00:00:00 2001 From: lena-larionova Date: Thu, 11 Jun 2026 12:33:47 -0700 Subject: [PATCH 2/4] cross-links; vale fixes; minor cleanup --- .../configure-oidc-with-auth-token-exchange.md | 1 + .../gateway/configure-oidc-with-multi-idp.md | 18 ++++++++++-------- .../oidc/diagrams/multi-idp-trusted-issuers.md | 3 ++- .../openid-connect/examples/extra-jwks.yaml | 4 +++- app/_kong_plugins/openid-connect/index.md | 16 +++++++++------- app/gateway/plugins/oidc/multi-idp.md | 8 ++++---- 6 files changed, 29 insertions(+), 21 deletions(-) diff --git a/app/_how-tos/gateway/configure-oidc-with-auth-token-exchange.md b/app/_how-tos/gateway/configure-oidc-with-auth-token-exchange.md index c02b35523b..9b060db6c0 100644 --- a/app/_how-tos/gateway/configure-oidc-with-auth-token-exchange.md +++ b/app/_how-tos/gateway/configure-oidc-with-auth-token-exchange.md @@ -11,6 +11,7 @@ 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 - text: OpenID Connect tutorials url: /how-to/?query=openid-connect diff --git a/app/_how-tos/gateway/configure-oidc-with-multi-idp.md b/app/_how-tos/gateway/configure-oidc-with-multi-idp.md index bafa9db260..ac3fc425cb 100644 --- a/app/_how-tos/gateway/configure-oidc-with-multi-idp.md +++ b/app/_how-tos/gateway/configure-oidc-with-multi-idp.md @@ -1,8 +1,8 @@ --- -title: Configure OpenID Connect with multiple IdPs using a trusted issuers registry +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 issuers registry. +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}} @@ -11,6 +11,8 @@ related_resources: 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 @@ -110,17 +112,17 @@ variables: 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. +* `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. +* `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: +Get a client credentials token from `realm-a`: ```sh TOKEN_A=$(curl -s -X POST \ @@ -146,7 +148,7 @@ When you decode the token in the forwarded `Authorization` header, the `iss` cla ### Token from realm-b -Get a client credentials token from realm-b: +Get a client credentials token from `realm-b`: ```sh TOKEN_B=$(curl -s -X POST \ diff --git a/app/_includes/plugins/oidc/diagrams/multi-idp-trusted-issuers.md b/app/_includes/plugins/oidc/diagrams/multi-idp-trusted-issuers.md index 5f5ba19a41..2cf7bb8244 100644 --- a/app/_includes/plugins/oidc/diagrams/multi-idp-trusted-issuers.md +++ b/app/_includes/plugins/oidc/diagrams/multi-idp-trusted-issuers.md @@ -1,3 +1,4 @@ + {% mermaid %} sequenceDiagram participant C as Client @@ -18,7 +19,6 @@ sequenceDiagram end K->>K: Verify signature K->>K: Check iss against issuers_allowed - K->>K: Verify exp, nbf K->>U: Proxy request with original token activate U U-->>K: Response @@ -26,3 +26,4 @@ sequenceDiagram K-->>C: Response deactivate K {% endmermaid %} + \ No newline at end of file diff --git a/app/_kong_plugins/openid-connect/examples/extra-jwks.yaml b/app/_kong_plugins/openid-connect/examples/extra-jwks.yaml index 72afdc77e3..a131f2a747 100644 --- a/app/_kong_plugins/openid-connect/examples/extra-jwks.yaml +++ b/app/_kong_plugins/openid-connect/examples/extra-jwks.yaml @@ -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: diff --git a/app/_kong_plugins/openid-connect/index.md b/app/_kong_plugins/openid-connect/index.md index c17153dafd..fbfae4730e 100644 --- a/app/_kong_plugins/openid-connect/index.md +++ b/app/_kong_plugins/openid-connect/index.md @@ -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. @@ -572,13 +574,13 @@ You can implement this in one of the following ways: {{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`. +* **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 %} +## 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. The RFC defines a protocol approach to support scenarios where a client can exchange a token for a new token by interacting with the authorization server. @@ -587,7 +589,7 @@ This is particularly useful in complex environments like microservices or cross- {:.info} > **Note**: The OpenID Connect plugin only supports exchanging access tokens. -#### Why use token exchange? +### Why use token exchange? Token exchange can be used in several critical use cases: @@ -602,7 +604,7 @@ For example, a frontend service needs to trade its token for a new token with sp > Because token exchange allows for the creation of new tokens, trust models are vital. The trust model must strictly define which clients are allowed to exchange tokens and which scopes they are permitted to elevate or downgrade to prevent security flaws like privilege escalations. -#### How token exchange works +### How token exchange works In a typical [OAuth flow](#kong-oauth-token-authentication-flow), a token is obtained to access a resource. However, in a token exchange, a client already has a token (the "subject token"). @@ -629,7 +631,7 @@ Set up token exchange: * [Example: Token transformation](/plugins/openid-connect/examples/token-exchange-transformation/) * [How-to: Configure OIDC with token exchange](/how-to/configure-oidc-with-token-exchange/) -##### Key terms +#### Key terms The token exchange flow uses the following terms: diff --git a/app/gateway/plugins/oidc/multi-idp.md b/app/gateway/plugins/oidc/multi-idp.md index 36f8256bcb..aa152856a9 100644 --- a/app/gateway/plugins/oidc/multi-idp.md +++ b/app/gateway/plugins/oidc/multi-idp.md @@ -1,5 +1,5 @@ --- -title: Multi-IdP token validation at the Gateway layer +title: Multi-IdP token validation with OpenID Connect description: "Configure the OpenID Connect plugin to validate tokens from multiple identity providers using a trusted issuer registry or token exchange." content_type: reference @@ -29,8 +29,7 @@ related_resources: url: /how-to/configure-oidc-with-token-exchange/ --- -If your APIs serve clients from multiple identity providers (IdPs), for example employees using Okta, B2B partners using Azure AD, or legacy systems on an in-house IdP, the [OpenID Connect (OIDC) plugin](/plugins/openid-connect/) can act as a federated authentication broker at the gateway layer. -Backends don't need per-IdP validation logic. +If your APIs serve clients from multiple identity providers (IdPs), (for example, employees using Okta, B2B partners using Azure AD, or legacy systems on an in-house IdP), the [OpenID Connect (OIDC) plugin](/plugins/openid-connect/) can act as a federated authentication broker at the gateway layer. In this setup, backends don't need per-IdP validation logic. {{site.base_gateway}} centralizes auth policy and forwards only the verified identity context upstream. The OIDC plugin supports two approaches for multi-IdP authentication, both using [JWT access token (bearer) auth](/plugins/openid-connect/#jwt-access-token-authentication-flow). @@ -64,6 +63,7 @@ rows: ## Option 1: Trusted issuers registry +In this approach, {{site.base_gateway}} acts as a federated authentication broker maintaining a registry of trusted issuers and their public key endpoints. The plugin inspects the `iss` claim of an incoming bearer token, looks up the matching JWKS endpoint from the configured list, validates the token signature and standard claims, then forwards the verified request upstream. No token transformation occurs. @@ -163,7 +163,7 @@ config: When a client from `idp-b` presents a bearer token, {{site.base_gateway}} validates it, then exchanges it with `idp-a` to produce a token the upstream service trusts. Tokens already issued by `idp-a` are validated as-is unless conditions require an exchange. -For more detail and complete walkthroughs: +For more detail, see: * [Plugin example: Token exchange for cross-domain security](/plugins/openid-connect/examples/token-exchange-cross-domain/) * [Plugin example: Token transformation](/plugins/openid-connect/examples/token-exchange-transformation/) * [How-to: Configure OpenID Connect with token exchange using Keycloak](/how-to/configure-oidc-with-token-exchange/) From 7037c0c6d3af303f800b35ab9e0a41017ed9504b Mon Sep 17 00:00:00 2001 From: lena-larionova Date: Thu, 11 Jun 2026 12:47:41 -0700 Subject: [PATCH 3/4] add missing url --- app/_how-tos/gateway/configure-oidc-with-auth-token-exchange.md | 1 + 1 file changed, 1 insertion(+) diff --git a/app/_how-tos/gateway/configure-oidc-with-auth-token-exchange.md b/app/_how-tos/gateway/configure-oidc-with-auth-token-exchange.md index 9b060db6c0..065c692762 100644 --- a/app/_how-tos/gateway/configure-oidc-with-auth-token-exchange.md +++ b/app/_how-tos/gateway/configure-oidc-with-auth-token-exchange.md @@ -12,6 +12,7 @@ related_resources: - 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 From 5ed7f5ceabecd39d18794c5cf216ccd9f09a7a1f Mon Sep 17 00:00:00 2001 From: lena-larionova <54370747+lena-larionova@users.noreply.github.com> Date: Fri, 12 Jun 2026 13:17:35 -0700 Subject: [PATCH 4/4] Apply suggestions from code review Co-authored-by: Diana <75819066+cloudjumpercat@users.noreply.github.com> --- app/_includes/prereqs/auth/oidc/keycloak-multi-idp.md | 8 ++++---- app/gateway/plugins/oidc/multi-idp.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/_includes/prereqs/auth/oidc/keycloak-multi-idp.md b/app/_includes/prereqs/auth/oidc/keycloak-multi-idp.md index af0fb7cffb..8113c5323c 100644 --- a/app/_includes/prereqs/auth/oidc/keycloak-multi-idp.md +++ b/app/_includes/prereqs/auth/oidc/keycloak-multi-idp.md @@ -31,7 +31,7 @@ The steps will be similar with other standard identity providers. export KEYCLOAK_HOST='localhost' ``` - Because we're using Docker for this demo, we have to configure a few networking parameters: + 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. @@ -67,14 +67,14 @@ rows: - section: "**Capability config**" settings: | * Toggle **Client authentication** to **on** - * Make sure that **Service accounts roles** is checked. + * Select the **Service accounts roles** checkbox. {% endtable %} Find the credentials for `client-a`: 1. In the sidebar, open **Clients**, and select `client-a`. -1. Open the **Credentials** tab. +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: @@ -113,7 +113,7 @@ rows: - section: "**Capability config**" settings: | * Toggle **Client authentication** to **on** - * Make sure that **Service accounts roles** is checked. + * Select the **Service accounts roles** checkbox. {% endtable %} diff --git a/app/gateway/plugins/oidc/multi-idp.md b/app/gateway/plugins/oidc/multi-idp.md index aa152856a9..68bab59dc6 100644 --- a/app/gateway/plugins/oidc/multi-idp.md +++ b/app/gateway/plugins/oidc/multi-idp.md @@ -187,7 +187,7 @@ rows: - error: Expired token log: "`invalid exp claim () was specified for access token`" cause: | - The token's `exp` claim is in the past. Get a fresh token from the IdP. + The token's `exp` claim is in the past. Get a new token from the IdP. - error: Signature verification failure log: "`invalid signature (pkey:verify: ...)`" cause: |