8000 Resolve Kubernetes IdP issuer from discovery by chance-coleman · Pull Request #50224 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content

Resolve Kubernetes IdP issuer from discovery - #50224

Merged
stianst merged 24 commits into
keycloak:mainfrom
chance-coleman:resolve-kubernetes-idp-issuer-discovery
Sep 23, 2026
Merged

stianst merged 24 commits into
keycloak:mainfrom
chance-coleman:resolve-kubernetes-idp-issuer-discovery

Conversation

@chance-coleman
@chance-coleman chance-coleman commented Jun 22, 2026 •
Copy link
Copy Markdown
Contributor

Closes #49039

Summary

This adds automatic OIDC issuer discovery for Kubernetes identity providers.

When the issuer discovery URL is configured, it is the source of truth. During identity provider creation or update,
Keycloak resolves the issuer and JWKS URL from the OIDC discovery document, validates them, and persists both values.
When the discovery URL is empty, Keycloak preserves a configured issuer directly. If neither an issuer URL nor a
discovery URL is configured, Keycloak continues to use the default Kubernetes issuer URL.

This supports managed Kubernetes clusters where service-account tokens use an external issuer, including EKS and AKS.
Discovery runs when the identity provider is saved or updated while a discovery URL is configured.

Service Account Token Forwarding

During save-time discovery, Keycloak uses the pod service-account token for the normalized default in-cluster Kubernetes API server URL:

https://kubernetes.default.svc

External discovery URLs are not sent the pod service-account token.

During runtime JWKS loading, Keycloak reuses the persisted JWKS URL when available. If a JWKS URL is not persisted, it falls back to issuer discovery. The existing issuer-match logic determines whether the service-account token is used: the token is included only when its iss claim matches the configured issuer.

Service-account tokens are trimmed before use, and empty tokens are ignored.

Admin Console

The Kubernetes identity provider form now supports:

  • Kubernetes Issuer URL
  • Issuer discovery URL

When the issuer discovery URL is configured, Keycloak refreshes the persisted issuer and JWKS URL from discovery when the provider is saved or updated. To use a direct issuer, leave the discovery URL empty. After saving, the Admin Console reloads the provider configuration so the resolved issuer and JWKS URL are displayed.

The form preserves SAML authentication context settings when it reloads the provider after saving.

Documentation

Updated the Kubernetes identity provider documentation to describe:

  • managed-cluster issuer discovery
  • configurable Kubernetes API server or external OIDC discovery URLs
  • save and update discovery behavior
  • persistence of the resolved issuer and JWKS URL
  • runtime service-account token behavior

Testing

Added coverage for:

  • resolving and persisting the issuer and JWKS URL during creation
  • handling discovery failures
  • skipping discovery when an issuer is configured directly
  • resolving the issuer during updates
  • clearing stale JWKS configuration when switching back to a direct issuer
  • duplicate resolved issuer validation
  • discovery URL normalization

Validated locally:

git diff --check
js/node_modules/.bin/prettier --check js/apps/admin-ui/src/identity-providers/add/DetailSettings.tsx
./mvnw -pl services -am -Dtest=KubernetesUtilsTest -DfailIfNoTests=false test

Copilot AI balanced review requested due to automatic review settings June 22, 2026 14:58
@chance-coleman
chance-coleman requested review from a team as code owners June 22, 2026 14:58
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds optional automatic OIDC issuer discovery for the Kubernetes identity provider by querying the configured Kubernetes API server discovery endpoint, persisting the resolved issuer before uniqueness validation, and tightening service account token forwarding so it’s only sent to trusted Kubernetes API server URLs (not to external/public JWKS endpoints).

Changes:

  • Add server-side config/options and validation-time issuer resolution via /.well-known/openid-configuration.
  • Restrict forwarding of the mounted service account token to “trusted” Kubernetes API server URLs for discovery/JWKS retrieval.
  • Add Admin UI fields, tests, and documentation for issuer discovery.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/base/src/test/java/org/keycloak/tests/client/authentication/external/KubernetesClientAuthTest.java Adds a test asserting external JWKS requests don’t receive an Authorization header.
tests/base/src/test/java/org/keycloak/tests/admin/identityprovider/IdentityProviderKubernetesTest.java Adds create/update validation tests for issuer discovery and related failure/uniqueness behavior.
test-framework/oauth/src/main/java/org/keycloak/testframework/oauth/OAuthIdentityProviderConfigBuilder.java Extends mock IdP config to allow specifying a discovery issuer.
test-framework/oauth/src/main/java/org/keycloak/testframework/oauth/OAuthIdentityProvider.java Enhances mock IdP to expose the last JWKS Authorization header and optionally return a configured issuer.
services/src/main/java/org/keycloak/broker/kubernetes/KubernetesUtils.java Introduces helpers for discovery URL construction, token loading, and trusted Kubernetes API URL detection.
services/src/main/java/org/keycloak/broker/kubernetes/KubernetesJwksEndpointLoader.java Reuses discovery URL helper and gates token forwarding based on trusted URL checks.
services/src/main/java/org/keycloak/broker/kubernetes/KubernetesIdentityProviderConfig.java Adds automatic issuer discovery and resolves/persists issuer during validation with timeouts and conditional auth.
js/apps/admin-ui/src/identity-providers/add/KubernetesSettings.tsx Adds UI toggle + conditional field for issuer discovery URL; disables manual issuer when enabled.
js/apps/admin-ui/src/identity-providers/add/AddKubernetesConnect.tsx Sets default for automaticIssuerDiscovery in the add-provider flow.
js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties Adds i18n strings for the new Kubernetes issuer discovery fields.
docs/documentation/server_admin/topics/identity-broker/kubernetes.adoc Documents issuer discovery behavior and token-forwarding constraints.

@slaskawi slaskawi left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fantastic work @chance-coleman !

Added a few comments. The most signifficant ones are for the default cases in auto-discovery.

I also noticed that one of the Operator tests is failing. We should definitely fix it and maybe even migrate to your new functionality?

@chance-coleman
chance-coleman force-pushed the resolve-kubernetes-idp-issuer-discovery branch from 4b93359 to f4bc061 Compare June 23, 2026 13:38
Copilot AI review requested due to automatic review settings June 23, 2026 13:38
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

@chance-coleman
chance-coleman force-pushed the resolve-kubernetes-idp-issuer-discovery branch from f4bc061 to 06d691d Compare June 23, 2026 14:31
Copilot AI review requested due to automatic review settings June 23, 2026 17:58
@chance-coleman
chance-coleman force-pushed the resolve-kubernetes-idp-issuer-discovery branch from 06d691d to 28f7319 Compare June 23, 2026 17:58
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

@chance-coleman
chance-coleman force-pushed the resolve-kubernetes-idp-issuer-discovery branch from 28f7319 to 6baa04c Compare June 23, 2026 19:34
Copilot AI review requested due to automatic review settings June 23, 2026 20:17
@chance-coleman
chance-coleman force-pushed the resolve-kubernetes-idp-issuer-discovery branch from 6baa04c to 5e6c603 Compare June 23, 2026 20:17
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.

@chance-coleman
chance-coleman force-pushed the resolve-kubernetes-idp-issuer-discovery branch from 5e6c603 to 17e881b Compare June 23, 2026 21:22
@chance-coleman
chance-coleman force-pushed the resolve-kubernetes-idp-issuer-discovery branch from 17e881b to 664bf21 Compare June 24, 2026 13:36
@keycloak-github-bot keycloak-github-bot Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unreported flaky test detected, please review

@keycloak-github-bot
Copy link
Copy Markdown

Unreported flaky test detected

If the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR.

org.keycloak.testsuite.federation.ldap.LDAPGroupMapperTest#test01_ldapOnlyGroupMappings

Keycloak CI - Base IT (5)

org.opentest4j.AssertionFailedError: expected: <4> but was: <3>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertEquals.failNotEqual(AssertEquals.java:197)
	at org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:150)
...

Report flaky test

Signed-off-by: Chance Coleman <chance@defenseunicorns.com>
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.

Signed-off-by: Chance Coleman <chance@defenseunicorns.com>
Signed-off-by: Chance Coleman <chance@defenseunicorns.com>
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Case-sensitive hostname trust checks can reject valid in-cluster Kubernetes endpoints.

Review details

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

services/src/main/java/org/keycloak/broker/kubernetes/KubernetesUtils.java:83

  • DNS hostnames are case-insensitive, but this exact comparison rejects valid equivalents such as https://Kubernetes.Default.SVC. That prevents the service-account token from being attached and can make authenticated in-cluster discovery fail.

This issue also appears on line 90 of the same file.

services/src/main/java/org/keycloak/broker/kubernetes/KubernetesUtils.java:90

  • The configured Kubernetes service host is also compared case-sensitively. When KUBERNETES_SERVICE_HOST is a hostname, an equivalent URL with different casing is incorrectly treated as external and receives no authentication token.
        if (!normalizedHost.equals(serviceHost)) {
  • Files reviewed: 18/18 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Signed-off-by: Chance Coleman <chance@defenseunicorns.com>
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Current trust rules can forward the pod token to untrusted hosts, and the Admin Console has discovery-state defects.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (3)

Previously missed (2) — in code that hasn't changed since the last review.

js/apps/admin-ui/src/identity-providers/add/KubernetesSettings.tsx:87

  • FormGroupField expects the translation key and derives its help key as ${label}Help. Passing the translated label makes the help lookup use Issuer discovery modeHelp, so the newly added kubernetesIssuerDiscoveryModeHelp text is never displayed.
    tests/base/src/test/java/org/keycloak/tests/admin/ConsentsTest.java:92
  • These consent-test lifecycle and cleanup changes are unrelated to Kubernetes issuer discovery and are not explained or validated by this PR. Revert them here or move them to a focused pull request with their own rationale and test results.

js/apps/admin-ui/src/identity-providers/add/KubernetesSettings.tsx:45

  • Ignoring an empty watched URL leaves mode stale after React Hook Form resets. For an upgraded provider with no stored issuerDiscoveryUrl, selecting External and then clicking Revert leaves External selected with a blank editable URL instead of restoring the original in-cluster mode; synchronize this local state with form reset/default state.
  useEffect(() => {
    if (issuerDiscoveryUrl) {
      setMode(discoveryMode(issuerDiscoveryUrl));
    }
  }, [issuerDiscoveryUrl]);
  • Files reviewed: 18/18 changed files
  • Comments generated: 2
  • Review effort level: Balanced

Signed-off-by: Chance Coleman <chance@defenseunicorns.com>
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The Admin Console can repeat discovery after saving and the discovery-mode help translation is broken.

Review details

Suppressed comments (2)

Previously missed (2) — in code that hasn't changed since the last review.

services/src/main/java/org/keycloak/broker/kubernetes/KubernetesIdentityProviderConfig.java:85

  • When the Admin Console clears the issuer to enter this branch, DetailSettings.tsx:380 resets the form to the submitted object, which still has an empty issuer rather than this persisted value. Every later save in that session therefore performs discovery again and can replace the issuer; refetch and reset from the stored IdP after updating.
    js/apps/admin-ui/src/identity-providers/add/KubernetesSettings.tsx:87
  • FormGroupField translates both label and ${label}Help internally, so passing the already translated text makes the help lookup use Issuer discovery modeHelp instead of the new kubernetesIssuerDiscoveryModeHelp key. Pass the translation key directly.
  • Files reviewed: 18/18 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

Signed-off-by: Chance Coleman <chance@defenseunicorns.com>
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.

Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.

Signed-off-by: Chance Coleman <chance@defenseunicorns.com>
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 4 comments.

Suppressed comments (1)

services/src/main/java/org/keycloak/broker/kubernetes/KubernetesJwksEndpointLoader.java:56

  • With persisted jwksUrl support, the service-account token can now be attached to the JWKS request for any configured jwksUri (including off-cluster URLs), as long as iss matches issuer. That can leak the pod token to external endpoints. Consider restricting token attachment to in-cluster endpoints (e.g., only when jwksUri is under the normalized DEFAULT_KUBERNETES_API_SERVER_URL / issuer host), or explicitly avoid adding auth when jwksUrl is configured and does not match the in-cluster discovery base.
        String token = getToken(issuer);
        String jwksUri = jwksUrl;
        if (Strings.isEmpty(jwksUri)) {
            String wellKnownEndpoint = KubernetesUtils.discoveryUrl(issuer);
            SimpleHttpRequest wellKnownRequest = simpleHttp.doGet(wellKnownEndpoint).acceptJson();
            if (token != null) {
                wellKnownRequest.auth(token);
            }

            jwksUri = wellKnownRequest.asJson(OIDCConfigurationRepresentation.class).getJwksUri();
            if (jwksUri == null) {
                throw new IOException("OIDC discovery document from " + wellKnownEndpoint + " did not include a jwks_uri");
            }
        }

        SimpleHttpRequest jwksRequest = simpleHttp.doGet(jwksUri).header(HttpHeaders.ACCEPT, "application/jwk-set+json");
        if (token != null) {
            jwksRequest.auth(token);
        }

Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.

@keycloak-github-bot
Copy link
Copy Markdown

Unreported flaky test detected

If the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR.

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#testAdapterRoleMappings

Keycloak CI - Adapter IT Strict Cookies

org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
	at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
...

Report flaky test

@keycloak-github-bot keycloak-github-bot Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unreported flaky test detected, please review

@stianst
stianst commented Sep 21, 2026
Copy link
Copy Markdown
Contributor

One more thing; when using the discovery url, and when the issuer url changes, I can get that updated by removing the issuer url, which is okay, but there's no way to get the saved jwks url updated. We could perhaps simplify it by not persisting the jwks url and just load it dynamically as before. Alternative, I guess we could expose jwks url in the admin console, or when the provider is updated with a discovery url we always check the issuer and jwks and update if it has changed.

Signed-off-by: Chance Coleman <139784371+chance-coleman@users.noreply.github.com>
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Signed-off-by: Chance Coleman <chance@defenseunicorns.com>
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Token forwarding can expose the pod credential, and issuer precedence plus SAML form reloading currently behave incorrectly.

Get a fresh assessment by requesting another Copilot review.

Review effort: Balanced
Findings: 1 High severity · 2 Medium severity

Open (3)
Resolved since last review (2)

Signed-off-by: Chance Coleman <chance@defenseunicorns.com>
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer 10BC8 look

JWKS token forwarding remains unconditional for matching issuers, leaving a linked compatibility requirement unresolved.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
Resolved since last review (2)
Previously missed (1)

In code that hasn't changed since last review

Medium severity Avoid forwarding service-account tokens to JWKS based solely on issuer

services/​src/​main/​java/​org/​keycloak/​broker/​kubernetes/​KubernetesJwksEndpointLoader.java:51

This still sends the pod service-account token on every JWKS request whenever its iss matches the configured issuer—which is normally true after managed-cluster discovery. That leaves the #49039 requirement for JWKS endpoints that reject bearer authentication unresolved; token forwarding needs an independent configuration/decision rather than issuer equality alone.

        SimpleHttpRequest jwksRequest = simpleHttp.doGet(jwksUri).header(HttpHeaders.ACCEPT, "application/jwk-set+json");
        if (token != null) {
            jwksRequest.auth(token);

@keycloak-github-bot keycloak-github-bot Bot left a comment
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unreported flaky test detected, please review

@keycloak-github-bot
Copy link
Copy Markdown

Unreported flaky test detected

If the flaky tests below are affected by the changes, please review and update the changes accordingly. Otherwise, a maintainer should report the flaky tests prior to merging the PR.

org.keycloak.testsuite.adapter.servlet.SAMLServletAdapterTest#salesPostSigPersistentTest

Keycloak CI - Adapter IT Strict Cookies

org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
	at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
...

Report flaky test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Signed JWT - Federated: support managed Kubernetes (EKS, GKE, AKS) via dynamic OIDC issuer discovery

5 participants

0