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..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 @@ -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 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..ac3fc425cb --- /dev/null +++ b/app/_how-tos/gateway/configure-oidc-with-multi-idp.md @@ -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. + +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..2cf7bb8244 --- /dev/null +++ b/app/_includes/plugins/oidc/diagrams/multi-idp-trusted-issuers.md @@ -0,0 +1,29 @@ + +{% 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->>U: Proxy request with original token + activate U + U-->>K: Response + deactivate U + K-->>C: Response + deactivate K +{% endmermaid %} + \ No newline at end of file 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..8113c5323c --- /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 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: + + +{% 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 %} + + +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: + + +{% 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 %} + + +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/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 ccf287b0ff..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. @@ -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. diff --git a/app/gateway/plugins/oidc/multi-idp.md b/app/gateway/plugins/oidc/multi-idp.md new file mode 100644 index 0000000000..68bab59dc6 --- /dev/null +++ b/app/gateway/plugins/oidc/multi-idp.md @@ -0,0 +1,197 @@ +--- +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 +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. 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). +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 + +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. + +{% 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, 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/) +* [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 new 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 %}