From 662405f86906cb7fd0967fa525e84c27a73dd641 Mon Sep 17 00:00:00 2001 From: UnicornChance Date: Tue, 2 Jun 2026 06:19:14 -0700 Subject: [PATCH] fix: resolve Kubernetes IdP issuer from discovery Closes #49039 Signed-off-by: UnicornChance --- .../topics/identity-broker/kubernetes.adoc | 10 +- .../admin/messages/messages_en.properties | 4 +- .../add/AddKubernetesConnect.tsx | 5 +- .../add/KubernetesSettings.tsx | 33 ++++- .../identity-providers/kubernetes.spec.ts | 15 +++ .../kubernetes/KubernetesConstants.java | 1 + .../KubernetesIdentityProviderConfig.java | 126 ++++++++++++++++++ .../oauth/OAuthIdentityProvider.java | 22 ++- .../OAuthIdentityProviderConfigBuilder.java | 16 ++- .../IdentityProviderKubernetesTest.java | 70 ++++++++++ .../external/KubernetesClientAuthTest.java | 2 +- 11 files changed, 291 insertions(+), 13 deletions(-) diff --git a/docs/documentation/server_admin/topics/identity-broker/kubernetes.adoc b/docs/documentation/server_admin/topics/identity-broker/kubernetes.adoc index 34ee747744aa..af6d216c6602 100644 --- a/docs/documentation/server_admin/topics/identity-broker/kubernetes.adoc +++ b/docs/documentation/server_admin/topics/identity-broker/kubernetes.adoc @@ -7,7 +7,10 @@ A Kubernetes identity provider supports authenticating clients with Kubernetes s This feature can be enabled and disabled with the feature `kubernetes-service-accounts` which is enabled by default. It depends on the feature `client-auth-federated` which is enabled by default as well. The default issuer URL for a Kubernetes cluster is `\https://kubernetes.default.svc.cluster.local`. You can discover -this value by decoding a service account token to retrieve the value of the `iss` claim. +this value by decoding a service account token to retrieve the value of the `iss` claim. Managed Kubernetes clusters +can expose the discovery endpoint through the in-cluster Kubernetes API while issuing service account tokens with a +different issuer URL. In this case, enable automatic issuer discovery. {project_name} resolves the issuer from the +in-cluster discovery endpoint and stores the discovered issuer for client assertion validation. Keycloak must be able to invoke the endpoint `/.well-known/openid-configuration` and additionally the JWKS endpoint returned in the well-known configuration. By default, these endpoints require authentication with a @@ -39,7 +42,10 @@ and make sure tokens can not be intercepted in communication between the client |The alias for the identity provider is used to link a client to the provider |Kubernetes Issuer URL -|The issuer URL of service account tokens. The URL `.well-known/openid-configuration` must be available to {project_name}) +|The issuer URL of service account tokens. The URL `/.well-known/openid-configuration` must be available to {project_name} when automatic issuer discovery is disabled + +|Automatic issuer discovery +|Resolve the service account token issuer from the in-cluster Kubernetes discovery endpoint. Use this option when a managed cluster exposes discovery through the Kubernetes API but uses a different issuer in service account tokens |=== + diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties index 6f3dc61cf1fe..48b5c64aa9b1 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties @@ -969,8 +969,10 @@ addKubernetesProvider=Add Kubernetes provider spiffeTrustDomain=SPIFFE Trust Domain spiffeTrustDomainHelp=Use a URL starting with 'spiffe://' followed by a domain name. For example, 'spiffe://acme.com'. spiffeBundleEndpoint=SPIFFE Bundle or OIDC JWKs endpoint -kubernetesIssuerUrlHelp=The issuer of the Kubernetes service account tokens +kubernetesIssuerUrlHelp=The issuer of the Kubernetes service account tokens. Disable automatic issuer discovery to edit this value manually. kubernetesIssuerUrl=Kubernetes Issuer URL +automaticIssuerDiscovery=Automatic issuer discovery +automaticIssuerDiscoveryHelp=Resolve the Kubernetes service account token issuer from the in-cluster Kubernetes discovery endpoint. When enabled, Keycloak uses the default Kubernetes discovery URL and stores the discovered issuer. permission=Permission saveEventListeners=Save Event Listeners capabilityConfig=Capability config diff --git a/js/apps/admin-ui/src/identity-providers/add/AddKubernetesConnect.tsx b/js/apps/admin-ui/src/identity-providers/add/AddKubernetesConnect.tsx index d1d89b76a79d..fec3bf53a525 100644 --- a/js/apps/admin-ui/src/identity-providers/add/AddKubernetesConnect.tsx +++ b/js/apps/admin-ui/src/identity-providers/add/AddKubernetesConnect.tsx @@ -29,7 +29,10 @@ export default function AddKubernetesConnect() { const id = "kubernetes"; const form = useForm({ - defaultValues: { alias: id, config: { allowCreate: "true" } }, + defaultValues: { + alias: id, + config: { allowCreate: "true", automaticIssuerDiscovery: "false" }, + }, mode: "onChange", }); const { handleSubmit } = form; diff --git a/js/apps/admin-ui/src/identity-providers/add/KubernetesSettings.tsx b/js/apps/admin-ui/src/identity-providers/add/KubernetesSettings.tsx index 22cdf4bd3666..52edb0b252ba 100644 --- a/js/apps/admin-ui/src/identity-providers/add/KubernetesSettings.tsx +++ b/js/apps/admin-ui/src/identity-providers/add/KubernetesSettings.tsx @@ -1,13 +1,34 @@ import { TextControl } from "@keycloak/keycloak-ui-shared"; +import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { FormGroupField } from "../component/FormGroupField"; -import { Controller, useFormContext } from "react-hook-form"; +import { Controller, useFormContext, useWatch } from "react-hook-form"; import { TimeSelector } from "../../components/time-selector/TimeSelector"; import IdentityProviderRepresentation from "libs/keycloak-admin-client/lib/defs/identityProviderRepresentation"; +import { DefaultSwitchControl } from "../../components/SwitchControl"; + +const DEFAULT_KUBERNETES_ISSUER_URL = + "https://kubernetes.default.svc.cluster.local"; export const KubernetesSettings = () => { const { t } = useTranslation(); - const { control } = useFormContext(); + const { control, setValue } = + useFormContext(); + const automaticIssuerDiscovery = useWatch({ + control, + name: "config.automaticIssuerDiscovery", + }); + + useEffect(() => { + if (automaticIssuerDiscovery === "true") { + setValue("config.issuerDiscoveryUrl", DEFAULT_KUBERNETES_ISSUER_URL, { + shouldDirty: false, + }); + } else { + setValue("config.issuerDiscoveryUrl", "", { shouldDirty: false }); + } + }, [automaticIssuerDiscovery, setValue]); + return ( <> { }} /> + + { await goToIdentityProviders(page); await clickTableRowItem(page, "kubernetes"); + await expect( + page.getByTestId("config.automaticIssuerDiscovery"), + ).not.toBeChecked(); + await expect(page.getByTestId("config.issuer")).toBeEnabled(); + + await page + .getByTestId("config.automaticIssuerDiscovery") + .click({ force: true }); + await expect(page.getByTestId("config.issuer")).toBeDisabled(); + + await page + .getByTestId("config.automaticIssuerDiscovery") + .click({ force: true }); + await expect(page.getByTestId("config.issuer")).toBeEnabled(); + await page .getByTestId("config.issuer") .fill("https://kubernetes2.myorg.com"); diff --git a/services/src/main/java/org/keycloak/broker/kubernetes/KubernetesConstants.java b/services/src/main/java/org/keycloak/broker/kubernetes/KubernetesConstants.java index 57f73195c07b..10efbe6244e0 100644 --- a/services/src/main/java/org/keycloak/broker/kubernetes/KubernetesConstants.java +++ b/services/src/main/java/org/keycloak/broker/kubernetes/KubernetesConstants.java @@ -5,5 +5,6 @@ public interface KubernetesConstants { String KUBERNETES_SERVICE_HOST_KEY = "KUBERNETES_SERVICE_HOST"; String KUBERNETES_SERVICE_PORT_HTTPS_KEY = "KUBERNETES_SERVICE_PORT_HTTPS"; String SERVICE_ACCOUNT_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token"; + String DEFAULT_KUBERNETES_ISSUER_URL = "https://kubernetes.default.svc.cluster.local"; } diff --git a/services/src/main/java/org/keycloak/broker/kubernetes/KubernetesIdentityProviderConfig.java b/services/src/main/java/org/keycloak/broker/kubernetes/KubernetesIdentityProviderConfig.java index 0951c657bc0d..c3bf91ec1950 100644 --- a/services/src/main/java/org/keycloak/broker/kubernetes/KubernetesIdentityProviderConfig.java +++ b/services/src/main/java/org/keycloak/broker/kubernetes/KubernetesIdentityProviderConfig.java @@ -1,14 +1,37 @@ package org.keycloak.broker.kubernetes; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import org.keycloak.broker.oidc.IssuerValidation; +import org.keycloak.http.simple.SimpleHttp; +import org.keycloak.http.simple.SimpleHttpRequest; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderType; +import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; +import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; +import org.keycloak.util.Strings; +import org.keycloak.utils.KeycloakSessionUtil; + +import org.apache.http.client.config.RequestConfig; +import org.jboss.logging.Logger; + +import static org.keycloak.broker.kubernetes.KubernetesConstants.DEFAULT_KUBERNETES_ISSUER_URL; +import static org.keycloak.broker.kubernetes.KubernetesConstants.KUBERNETES_SERVICE_HOST_KEY; +import static org.keycloak.broker.kubernetes.KubernetesConstants.KUBERNETES_SERVICE_PORT_HTTPS_KEY; +import static org.keycloak.broker.kubernetes.KubernetesConstants.SERVICE_ACCOUNT_TOKEN_PATH; public class KubernetesIdentityProviderConfig extends IdentityProviderModel implements IssuerValidation { + private static final int DISCOVERY_TIMEOUT_MILLIS = 5000; + private static final Logger logger = Logger.getLogger(KubernetesIdentityProviderConfig.class); + static final String AUTOMATIC_ISSUER_DISCOVERY = "automaticIssuerDiscovery"; + static final String ISSUER_DISCOVERY_URL = "issuerDiscoveryUrl"; + public KubernetesIdentityProviderConfig() { } @@ -20,6 +43,10 @@ public String getIssuer() { return getConfig().get(ISSUER); } + public boolean isAutomaticIssuerDiscovery() { + return Boolean.parseBoolean(getConfig().get(AUTOMATIC_ISSUER_DISCOVERY)); + } + public int getAllowedClockSkew() { String allowedClockSkew = getConfig().get(ALLOWED_CLOCK_SKEW); if (allowedClockSkew == null || allowedClockSkew.isEmpty()) { @@ -41,6 +68,105 @@ public Boolean isHideOnLogin() { @Override public void validate(RealmModel realm) { super.validate(realm); + // Persist the discovered service account token issuer before issuer uniqueness validation. + resolveIssuer(); validateIssuer(realm, IdentityProviderType.CLIENT_ASSERTION); } + + private void resolveIssuer() { + if (!isAutomaticIssuerDiscovery()) { + return; + } + + String issuer = getIssuerDiscoveryUrl(); + getConfig().remove(ISSUER); + if (!isInClusterDiscoveryUrl(issuer)) { + throw new IllegalArgumentException("Automatic issuer discovery URL must be an HTTPS Kubernetes API service URL"); + } + + try { + KeycloakSession session = KeycloakSessionUtil.getKeycloakSession(); + SimpleHttpRequest request = SimpleHttp.create(session) + .withRequestConfig(RequestConfig.custom() + .setConnectTimeout(DISCOVERY_TIMEOUT_MILLIS) + .setSocketTimeout(DISCOVERY_TIMEOUT_MILLIS) + .build()) + .doGet(discoveryUrl(issuer)) + .acceptJson(); + + String token = getServiceAccountToken(); + if (!Strings.isEmpty(token)) { + request.auth(token); + } + + OIDCConfigurationRepresentation oidcConfig = request.asJson(OIDCConfigurationRepresentation.class); + + if (!Strings.isEmpty(oidcConfig.getIssuer())) { + getConfig().put(ISSUER, oidcConfig.getIssuer()); + } + } catch (Exception e) { + logger.warnf(e, "Failed to resolve Kubernetes issuer from '%s'", issuer); + } + } + + private String getIssuerDiscoveryUrl() { + String issuerDiscoveryUrl = getConfig().get(ISSUER_DISCOVERY_URL); + if (!Strings.isEmpty(issuerDiscoveryUrl)) { + return issuerDiscoveryUrl; + } + + return DEFAULT_KUBERNETES_ISSUER_URL; + } + + private String discoveryUrl(String issuer) { + int end = issuer.length(); + while (end > 0 && issuer.charAt(end - 1) == '/') { + end--; + } + return issuer.substring(0, end) + "/.well-known/openid-configuration"; + } + + private String getServiceAccountToken() throws Exception { + Path path = Path.of(SERVICE_ACCOUNT_TOKEN_PATH); + if (!Files.exists(path)) { + return null; + } + + return Files.readString(path, StandardCharsets.UTF_8); + } + + private boolean isInClusterDiscoveryUrl(String issuer) { + URI uri; + try { + uri = URI.create(issuer); + } catch (IllegalArgumentException e) { + return false; + } + + if (!"https".equals(uri.getScheme())) { + return false; + } + + String host = uri.getHost(); + if (host == null) { + return false; + } + + // Save-time discovery has no client assertion JWT yet, so it cannot use the runtime + // jwt.issuer comparison from KubernetesJwksEndpointLoader. Limit discovery to the + // in-cluster Kubernetes API service instead. + if ("kubernetes".equals(host) || "kubernetes.default".equals(host) || "kubernetes.default.svc".equals(host) || "kubernetes.default.svc.cluster.local".equals(host)) { + return true; + } + + String serviceHost = System.getenv(KUBERNETES_SERVICE_HOST_KEY); + if (!host.equals(serviceHost)) { + return false; + } + + String servicePort = System.getenv(KUBERNETES_SERVICE_PORT_HTTPS_KEY); + int port = uri.getPort(); + // No explicit port means HTTPS default port, which is the Kubernetes API service port. + return port == -1 || Strings.isEmpty(servicePort) || servicePort.equals(Integer.toString(port)); + } } diff --git a/test-framework/oauth/src/main/java/org/keycloak/testframework/oauth/OAuthIdentityProvider.java b/test-framework/oauth/src/main/java/org/keycloak/testframework/oauth/OAuthIdentityProvider.java index 874899309339..aaae1489c11c 100644 --- a/test-framework/oauth/src/main/java/org/keycloak/testframework/oauth/OAuthIdentityProvider.java +++ b/test-framework/oauth/src/main/java/org/keycloak/testframework/oauth/OAuthIdentityProvider.java @@ -21,6 +21,7 @@ import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation; import org.keycloak.representations.JsonWebToken; import org.keycloak.util.JsonSerialization; +import org.keycloak.util.Strings; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; @@ -33,22 +34,29 @@ */ public class OAuthIdentityProvider { + private static final String IDP_PATH = "/idp"; + private final HttpServer httpServer; private final OAuthIdentityProviderKeys keys; private final OAuthIdentityProviderConfigBuilder.OAuthIdentityProviderConfiguration config; + private final String issuer; private int keysRequestCount = 0; public OAuthIdentityProvider(HttpServer httpServer, OAuthIdentityProviderConfigBuilder.OAuthIdentityProviderConfiguration config) { this.config = config; + this.issuer = config.issuer(); if (!CryptoIntegration.isInitialised()) { CryptoIntegration.setProvider(new DefaultCryptoProvider()); } this.httpServer = httpServer; - httpServer.createContext("/idp/.well-known/openid-configuration", new WellKnownHandler()); - httpServer.createContext("/idp/jwks", new JwksHttpHandler()); + httpServer.createContext(IDP_PATH + "/.well-known/openid-configuration", new WellKnownHandler()); + if (!IDP_PATH.equals(config.discoveryPath())) { + httpServer.createContext(config.discoveryPath() + "/.well-known/openid-configuration", new WellKnownHandler()); + } + httpServer.createContext(IDP_PATH + "/jwks", new JwksHttpHandler()); keys = new OAuthIdentityProviderKeys(config); } @@ -78,8 +86,11 @@ public int getKeysRequestCount() { } public void close() { - httpServer.removeContext("/idp/.well-known/openid-configuration"); - httpServer.removeContext("/idp/jwks"); + httpServer.removeContext(IDP_PATH + "/.well-known/openid-configuration"); + if (!IDP_PATH.equals(config.discoveryPath())) { + httpServer.removeContext(config.discoveryPath() + "/.well-known/openid-configuration"); + } + httpServer.removeContext(IDP_PATH + "/jwks"); } public class WellKnownHandler implements HttpHandler { @@ -87,6 +98,9 @@ public class WellKnownHandler implements HttpHandler { @Override public void handle(HttpExchange exchange) throws IOException { OIDCConfigurationRepresentation oidcConfig = new OIDCConfigurationRepresentation(); + if (!Strings.isEmpty(issuer)) { + oidcConfig.setIssuer(issuer); + } oidcConfig.setJwksUri("http://127.0.0.1:8500/idp/jwks"); String oidcConfigString = JsonSerialization.writeValueAsString(oidcConfig); diff --git a/test-framework/oauth/src/main/java/org/keycloak/testframework/oauth/OAuthIdentityProviderConfigBuilder.java b/test-framework/oauth/src/main/java/org/keycloak/testframework/oauth/OAuthIdentityProviderConfigBuilder.java index a642b6266ab6..06c525cca186 100644 --- a/test-framework/oauth/src/main/java/org/keycloak/testframework/oauth/OAuthIdentityProviderConfigBuilder.java +++ b/test-framework/oauth/src/main/java/org/keycloak/testframework/oauth/OAuthIdentityProviderConfigBuilder.java @@ -4,6 +4,8 @@ public class OAuthIdentityProviderConfigBuilder { private Mode mode = Mode.DEFAULT; private boolean jwkUse = true; + private String issuer; + private String discoveryPath = "/idp"; public OAuthIdentityProviderConfigBuilder spiffe() { mode = Mode.SPIFFE; @@ -20,11 +22,21 @@ public OAuthIdentityProviderConfigBuilder jwkUse(boolean jwkUse) { return this; } + public OAuthIdentityProviderConfigBuilder issuer(String issuer) { + this.issuer = issuer; + return this; + } + + public OAuthIdentityProviderConfigBuilder discoveryPath(String discoveryPath) { + this.discoveryPath = discoveryPath; + return this; + } + public OAuthIdentityProviderConfiguration build() { - return new OAuthIdentityProviderConfiguration(mode, jwkUse); + return new OAuthIdentityProviderConfiguration(mode, jwkUse, issuer, discoveryPath); } - public record OAuthIdentityProviderConfiguration(Mode mode, boolean jwkUse) { + public record OAuthIdentityProviderConfiguration(Mode mode, boolean jwkUse, String issuer, String discoveryPath) { } public enum Mode { diff --git a/tests/base/src/test/java/org/keycloak/tests/admin/identityprovider/IdentityProviderKubernetesTest.java b/tests/base/src/test/java/org/keycloak/tests/admin/identityprovider/IdentityProviderKubernetesTest.java index f7b25a7c57fb..4c48743e8b87 100644 --- a/tests/base/src/test/java/org/keycloak/tests/admin/identityprovider/IdentityProviderKubernetesTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/admin/identityprovider/IdentityProviderKubernetesTest.java @@ -17,6 +17,8 @@ package org.keycloak.tests.admin.identityprovider; +import java.util.Map; + import jakarta.ws.rs.WebApplicationException; import jakarta.ws.rs.core.Response; @@ -36,6 +38,66 @@ @KeycloakIntegrationTest(config = IdentityProviderKubernetesTest.TestServerConfig.class) public class IdentityProviderKubernetesTest extends AbstractIdentityProviderTest { + private static final String DISCOVERY_ISSUER = "http://127.0.0.1:8500/idp"; + private static final String AUTOMATIC_ISSUER_DISCOVERY = "automaticIssuerDiscovery"; + private static final String ISSUER_DISCOVERY_URL = "issuerDiscoveryUrl"; + + @Test + public void testCreateIdentityProviderRejectsNonKubernetesIssuerDiscoveryUrl() { + IdentityProviderRepresentation identityProvider = createRep("kubernetes", "kubernetes"); + enableAutomaticIssuerDiscovery(identityProvider); + + try (Response response = managedRealm.admin().identityProviders().create(identityProvider)) { + assertEquals(400, response.getStatus(), () -> response.readEntity(String.class)); + ErrorRepresentation error = response.readEntity(ErrorRepresentation.class); + assertEquals("Automatic issuer discovery URL must be an HTTPS Kubernetes API service URL", error.getErrorMessage()); + } + } + + @Test + public void testUpdateIdentityProviderRejectsNonKubernetesIssuerDiscoveryUrl() { + IdentityProviderRepresentation identityProvider = createRep("kubernetes", "kubernetes"); + identityProvider.getConfig().put("issuer", "https://localhost"); + + try (Response response = managedRealm.admin().identityProviders().create(identityProvider)) { + assertEquals(201, response.getStatus(), () -> response.readEntity(String.class)); + } + managedRealm.cleanup().add(r -> r.identityProviders().get("kubernetes").remove()); + + IdentityProviderResource idpResource = managedRealm.admin().identityProviders().get("kubernetes"); + identityProvider = idpResource.toRepresentation(); + enableAutomaticIssuerDiscovery(identityProvider); + try { + idpResource.update(identityProvider); + Assertions.fail("Non-Kubernetes issuer discovery URL not detected"); + } catch (WebApplicationException ex) { + assertEquals(400, ex.getResponse().getStatus()); + ErrorRepresentation error = ex.getResponse().readEntity(ErrorRepresentation.class); + assertEquals("Automatic issuer discovery URL must be an HTTPS Kubernetes API service URL", error.getErrorMessage()); + } + + IdentityProviderRepresentation updated = idpResource.toRepresentation(); + assertEquals("https://localhost", updated.getConfig().get("issuer")); + } + + @Test + public void testIssuerDiscoveryUrlWithoutAutomaticDiscoveryDoesNotResolveIssuer() { + IdentityProviderRepresentation identityProvider = createRep("kubernetes", "kubernetes"); + identityProvider.setConfig(Map.of( + AUTOMATIC_ISSUER_DISCOVERY, "false", + "issuer", "https://localhost", + ISSUER_DISCOVERY_URL, DISCOVERY_ISSUER + )); + + try (Response response = managedRealm.admin().identityProviders().create(identityProvider)) { + assertEquals(201, response.getStatus(), () -> response.readEntity(String.class)); + } + managedRealm.cleanup().add(r -> r.identityProviders().get("kubernetes").remove()); + + IdentityProviderRepresentation created = managedRealm.admin().identityProviders().get("kubernetes").toRepresentation(); + assertEquals("https://localhost", created.getConfig().get("issuer")); + } + @Test public void testCreateIdentityProviderWithDuplicateIssuer() { IdentityProviderRepresentation identityProvider = createRep("kubernetes1", "kubernetes"); @@ -95,4 +157,12 @@ public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) return config.features(Profile.Feature.KUBERNETES_SERVICE_ACCOUNTS); } } + + private static void enableAutomaticIssuerDiscovery(IdentityProviderRepresentation identityProvider) { + identityProvider.setConfig(Map.of( + AUTOMATIC_ISSUER_DISCOVERY, "true", + "issuer", DISCOVERY_ISSUER, + ISSUER_DISCOVERY_URL, DISCOVERY_ISSUER + )); + } } diff --git a/tests/base/src/test/java/org/keycloak/tests/client/authentication/external/KubernetesClientAuthTest.java b/tests/base/src/test/java/org/keycloak/tests/client/authentication/external/KubernetesClientAuthTest.java index fb88d5ba530e..e7c85c31d0c2 100644 --- a/tests/base/src/test/java/org/keycloak/tests/client/authentication/external/KubernetesClientAuthTest.java +++ b/tests/base/src/test/java/org/keycloak/tests/client/authentication/external/KubernetesClientAuthTest.java @@ -149,7 +149,7 @@ public static class KubernetesIdpConfig implements OAuthIdentityProviderConfig { @Override public OAuthIdentityProviderConfigBuilder configure(OAuthIdentityProviderConfigBuilder config) { - return config.kubernetes(); + return config.kubernetes().issuer(ISSUER); } }