You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Resolve Kubernetes IdP issuer from discovery - #50224
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
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.
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.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)
...
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.
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.
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.
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.
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]);
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.
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.
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.
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.
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);
}
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.
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.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)
...
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.
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🟡 Changes recommended
Disabling discovery while retaining the resolved issuer can leave a stale JWKS URL active, and the configuration guidance contradicts the supported default fallback.
Get a fresh assessment by requesting another Copilot review.
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.
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.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)
...
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
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.
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.svcExternal 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
issclaim 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:
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:
Testing
Added coverage for:
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