-
Notifications
You must be signed in to change notification settings - Fork 99
feat(gateway): OIDC multi-idp reference and guide for extra_jwks_urls #5550
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
|
|
||
| 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. | ||
29 changes: 29 additions & 0 deletions
29
app/_includes/plugins/oidc/diagrams/multi-idp-trusted-issuers.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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--> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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' | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.