From a8f21be8fdae0ff284d2410e9eb97c38a5f5c696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Vav=C5=99=C3=ADk?= Date: Wed, 2 Oct 2024 14:41:51 +0200 Subject: [PATCH] Start Keycloak Dev Svc for standalone OIDC Client --- bom/application/pom.xml | 5 + docs/src/main/asciidoc/dev-services.adoc | 2 +- ...dc-configuration-properties-reference.adoc | 2 +- extensions/devservices/keycloak/pom.xml | 67 +++++++ .../keycloak/KeycloakDevServicesConfig.java} | 186 +++++------------- .../KeycloakDevServicesConfigBuildItem.java | 4 +- .../KeycloakDevServicesProcessor.java | 152 ++++++++------ .../KeycloakDevServicesRequiredBuildItem.java | 47 +++++ .../keycloak/KeycloakDevServicesUtils.java | 96 +++++++++ .../main/resources/dev-service/upconfig.json | 0 extensions/devservices/pom.xml | 1 + extensions/oidc-client/deployment/pom.xml | 2 +- .../deployment/OidcClientBuildStep.java | 28 +++ .../OidcClientKeycloakDevServiceTest.java | 63 ++++++ ...tPasswordGrantSecretIsMissingTestCase.java | 2 +- ...tTooManyJwtCredentialKeyPropsTestCase.java | 2 +- .../oidc-client-dev-service-test.properties | 15 ++ .../resources/META-INF/quarkus-extension.yaml | 1 + extensions/oidc/deployment/pom.xml | 34 ++-- .../quarkus/oidc/deployment/DevUiConfig.java | 1 - .../oidc/deployment/OidcBuildStep.java | 9 + .../devservices/AbstractDevUIProcessor.java | 6 - .../devservices/OidcDevServicesBuildItem.java | 11 -- .../keycloak/KeycloakBuildTimeConfig.java | 18 -- .../keycloak/KeycloakDevUIProcessor.java | 6 +- 25 files changed, 500 insertions(+), 260 deletions(-) create mode 100644 extensions/devservices/keycloak/pom.xml rename extensions/{oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java => devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfig.java} (54%) rename extensions/{oidc/deployment/src/main/java/io/quarkus/oidc/deployment => devservices/keycloak/src/main/java/io/quarkus}/devservices/keycloak/KeycloakDevServicesConfigBuildItem.java (89%) rename extensions/{oidc/deployment/src/main/java/io/quarkus/oidc/deployment => devservices/keycloak/src/main/java/io/quarkus}/devservices/keycloak/KeycloakDevServicesProcessor.java (88%) create mode 100644 extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesRequiredBuildItem.java create mode 100644 extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesUtils.java rename extensions/{oidc/deployment => devservices/keycloak}/src/main/resources/dev-service/upconfig.json (100%) create mode 100644 extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientKeycloakDevServiceTest.java create mode 100644 extensions/oidc-client/deployment/src/test/resources/oidc-client-dev-service-test.properties delete mode 100644 extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevServicesBuildItem.java delete mode 100644 extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakBuildTimeConfig.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index da4a6a7614d52..2e9b3d7f5bf77 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -1044,6 +1044,11 @@ quarkus-keycloak-admin-client-common-deployment ${project.version} + + io.quarkus + quarkus-devservices-keycloak + ${project.version} + io.quarkus quarkus-flyway diff --git a/docs/src/main/asciidoc/dev-services.adoc b/docs/src/main/asciidoc/dev-services.adoc index ab8b767334837..0f1e790c1e7ed 100644 --- a/docs/src/main/asciidoc/dev-services.adoc +++ b/docs/src/main/asciidoc/dev-services.adoc @@ -102,7 +102,7 @@ The Keycloak Dev Service will be enabled when the `quarkus-oidc` extension is pr the server address has not been explicitly configured. More information can be found in the xref:security-openid-connect-dev-services.adoc[OIDC Dev Services Guide]. -include::{generated-dir}/config/quarkus-oidc_quarkus.keycloak.devservices.adoc[opts=optional, leveloffset=+1] +include::{generated-dir}/config/quarkus-devservices-keycloak_quarkus.keycloak.adoc[opts=optional, leveloffset=+1] === Kubernetes diff --git a/docs/src/main/asciidoc/security-oidc-configuration-properties-reference.adoc b/docs/src/main/asciidoc/security-oidc-configuration-properties-reference.adoc index 6ada18cfc2b2c..82918a1bfad9a 100644 --- a/docs/src/main/asciidoc/security-oidc-configuration-properties-reference.adoc +++ b/docs/src/main/asciidoc/security-oidc-configuration-properties-reference.adoc @@ -19,7 +19,7 @@ include::{generated-dir}/config/quarkus-oidc_quarkus.oidc.adoc[opts=optional, le == Keycloak Dev Services configuration -include::{generated-dir}/config/quarkus-oidc_quarkus.keycloak.adoc[opts=optional, leveloffset=+1] +include::{generated-dir}/config/quarkus-devservices-keycloak_quarkus.keycloak.adoc[opts=optional, leveloffset=+1] == References diff --git a/extensions/devservices/keycloak/pom.xml b/extensions/devservices/keycloak/pom.xml new file mode 100644 index 0000000000000..a12beb74331f8 --- /dev/null +++ b/extensions/devservices/keycloak/pom.xml @@ -0,0 +1,67 @@ + + + + quarkus-devservices-parent + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-devservices-keycloak + Quarkus - DevServices - Keycloak + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-devservices-common + + + io.quarkus + quarkus-vertx-deployment + + + io.quarkus + quarkus-mutiny-deployment + + + io.smallrye.reactive + smallrye-mutiny-vertx-web-client + + + org.keycloak + keycloak-core + + + com.sun.activation + jakarta.activation + + + + + + + + maven-compiler-plugin + + + default-compile + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + + diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfig.java similarity index 54% rename from extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java rename to extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfig.java index 722f5cca7994e..5652b15cc1ff9 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/DevServicesConfig.java +++ b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfig.java @@ -1,19 +1,23 @@ -package io.quarkus.oidc.deployment.devservices.keycloak; +package io.quarkus.devservices.keycloak; +import java.time.Duration; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.OptionalInt; -import io.quarkus.oidc.deployment.DevUiConfig; import io.quarkus.runtime.annotations.ConfigDocMapKey; -import io.quarkus.runtime.annotations.ConfigGroup; -import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; import io.quarkus.runtime.configuration.MemorySize; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; -@ConfigGroup -public class DevServicesConfig { +/** + * Build time configuration for the Keycloak Dev Service. + */ +@ConfigMapping(prefix = "quarkus.keycloak.devservices") +@ConfigRoot +public interface KeycloakDevServicesConfig { /** * Flag to enable (default) or disable Dev Services. @@ -21,8 +25,8 @@ public class DevServicesConfig { * When enabled, Dev Services for Keycloak automatically configures and starts Keycloak in Dev or Test mode, and when Docker * is running. */ - @ConfigItem(defaultValue = "true") - public boolean enabled = true; + @WithDefault("true") + boolean enabled(); /** * The container image name for Dev Services providers. @@ -35,8 +39,8 @@ public class DevServicesConfig { * ends with `-legacy`. * Override with `quarkus.keycloak.devservices.keycloak-x-image`. */ - @ConfigItem(defaultValue = "quay.io/keycloak/keycloak:25.0.6") - public String imageName; + @WithDefault("quay.io/keycloak/keycloak:25.0.6") + String imageName(); /** * Indicates if a Keycloak-X image is used. @@ -45,8 +49,7 @@ public class DevServicesConfig { * For custom images, override with `quarkus.keycloak.devservices.keycloak-x-image`. * You do not need to set this property if the default check works. */ - @ConfigItem - public Optional keycloakXImage; + Optional keycloakXImage(); /** * Determines if the Keycloak container is shared. @@ -60,19 +63,9 @@ public class DevServicesConfig { * * Container sharing is available only in dev mode. */ - @ConfigItem(defaultValue = "true") - public boolean shared; + @WithDefault("true") + boolean shared(); - /** - * The value of the {@code quarkus-dev-service-keycloak} label attached to the started container. - * This property is used when {@code shared} is set to {@code true}. - * In this case, before starting a container, Dev Services for Keycloak looks for a container with the - * {@code quarkus-dev-service-keycloak} label - * set to the configured value. If found, it uses this container instead of starting a new one. Otherwise, it - * starts a new container with the {@code quarkus-dev-service-keycloak} label set to the specified value. - *

- * Container sharing is only used in dev mode. - */ /** * The value of the `quarkus-dev-service-keycloak` label for identifying the Keycloak container. * @@ -81,8 +74,8 @@ public class DevServicesConfig { * * Applicable only in dev mode. */ - @ConfigItem(defaultValue = "quarkus") - public String serviceName; + @WithDefault("quarkus") + String serviceName(); /** * A comma-separated list of class or file system paths to Keycloak realm files. @@ -92,44 +85,40 @@ public class DevServicesConfig { * To learn more about Keycloak realm files, consult the Importing * and Exporting Keycloak Realms documentation. */ - @ConfigItem - public Optional> realmPath; + Optional> realmPath(); /** * Aliases to additional class or file system resources that are used to initialize Keycloak. * Each map entry represents a mapping between an alias and a class or file system resource path. */ - @ConfigItem @ConfigDocMapKey("alias-name") - public Map resourceAliases; + Map resourceAliases(); + /** * Additional class or file system resources that are used to initialize Keycloak. * Each map entry represents a mapping between a class or file system resource path alias and the Keycloak container * location. */ - @ConfigItem @ConfigDocMapKey("resource-name") - public Map resourceMappings; + Map resourceMappings(); /** * The `JAVA_OPTS` passed to the keycloak JVM */ - @ConfigItem - public Optional javaOpts; + Optional javaOpts(); /** * Show Keycloak log messages with a "Keycloak:" prefix. */ - @ConfigItem(defaultValue = "false") - public boolean showLogs; + @WithDefault("false") + boolean showLogs(); /** * Keycloak start command. * Use this property to experiment with Keycloak start options, see {@link https://www.keycloak.org/server/all-config}. * Note, it is ignored when loading legacy Keycloak WildFly images. */ - @ConfigItem - public Optional startCommand; + Optional startCommand(); /** * The name of the Keycloak realm. @@ -139,8 +128,7 @@ public class DevServicesConfig { * It is recommended to always set this property so that Dev Services for Keycloak can identify the realm name without * parsing the realm file. */ - @ConfigItem - public Optional realmName; + Optional realmName(); /** * Specifies whether to create the Keycloak realm when no realm file is found at the `realm-path`. @@ -148,20 +136,23 @@ public class DevServicesConfig { * Set to `false` if the realm is to be created using either the Keycloak Administration Console or * the Keycloak Admin API provided by {@linkplain io.quarkus.test.common.QuarkusTestResourceLifecycleManager}. */ - @ConfigItem(defaultValue = "true") - public boolean createRealm; + @WithDefault("true") + boolean createRealm(); /** - * Specifies whether to create the default client id `quarkus-app` with a secret `secret`and register them as - * `quarkus.oidc.client.id` and `quarkus.oidc.credentials.secret` properties, if the {@link #createRealm} property is set to - * true. + * Specifies whether to create the default client id `quarkus-app` with a secret `secret`and register them + * if the {@link #createRealm} property is set to true. + * For OIDC extension configuration properties `quarkus.oidc.client.id` and `quarkus.oidc.credentials.secret` will + * be configured. + * For OIDC Client extension configuration properties `quarkus.oidc-client.client.id` + * and `quarkus.oidc-client.credentials.secret` will be configured. * * Set to `false` if clients have to be created using either the Keycloak Administration Console or * the Keycloak Admin API provided by {@linkplain io.quarkus.test.common.QuarkusTestResourceLifecycleManager} * or registered dynamically. */ - @ConfigItem(defaultValue = "true") - public boolean createClient; + @WithDefault("true") + boolean createClient(); /** * Specifies whether to start the container even if the default OIDC tenant is disabled. @@ -169,8 +160,8 @@ public class DevServicesConfig { * Setting this property to true may be necessary in a multi-tenant OIDC setup, especially when OIDC tenants are created * dynamically. */ - @ConfigItem(defaultValue = "false") - public boolean startWithDisabledTenant = false; + @WithDefault("false") + boolean startWithDisabledTenant(); /** * A map of Keycloak usernames to passwords. @@ -178,8 +169,7 @@ public class DevServicesConfig { * If empty, default users `alice` and `bob` are created with their names as passwords. * This map is used for user creation when no realm file is found at the `realm-path`. */ - @ConfigItem - public Map users; + Map users(); /** * A map of roles for Keycloak users. @@ -188,104 +178,36 @@ public class DevServicesConfig { * `user` role. * This map is used for role creation when no realm file is found at the `realm-path`. */ - @ConfigItem @ConfigDocMapKey("role-name") - public Map> roles; - - /** - * Specifies the grant type. - * - * @deprecated This field is deprecated. Use {@link DevUiConfig#grant} instead. - */ - @Deprecated - public Grant grant = new Grant(); - - @ConfigGroup - public static class Grant { - public static enum Type { - /** - * `client_credentials` grant - */ - CLIENT("client_credentials"), - /** - * `password` grant - */ - PASSWORD("password"), - - /** - * `authorization_code` grant - */ - CODE("code"), - - /** - * `implicit` grant - */ - IMPLICIT("implicit"); - - private String grantType; - - private Type(String grantType) { - this.grantType = grantType; - } - - public String getGrantType() { - return grantType; - } - } - - /** - * Defines the grant type for aquiring tokens for testing OIDC `service` applications. - */ - @ConfigItem(defaultValue = "code") - public Type type = Type.CODE; - } + Map> roles(); /** * The specific port for the dev service to listen on. *

* If not specified, a random port is selected. */ - @ConfigItem - public OptionalInt port; + OptionalInt port(); /** * Environment variables to be passed to the container. */ - @ConfigItem @ConfigDocMapKey("environment-variable-name") - public Map containerEnv; + Map containerEnv(); /** * Memory limit for Keycloak container *

* If not specified, 750MiB is the default memory limit. */ - @ConfigItem(defaultValue = "750M") - public MemorySize containerMemoryLimit; + @WithDefault("750M") + MemorySize containerMemoryLimit(); - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - DevServicesConfig that = (DevServicesConfig) o; - // grant.type is not checked since it only affects which grant is used by the Dev UI provider.html - // and as such the changes to this property should not cause restarting a container - return enabled == that.enabled - && Objects.equals(imageName, that.imageName) - && Objects.equals(port, that.port) - && Objects.equals(realmPath, that.realmPath) - && Objects.equals(realmName, that.realmName) - && Objects.equals(users, that.users) - && Objects.equals(javaOpts, that.javaOpts) - && Objects.equals(roles, that.roles) - && Objects.equals(containerEnv, that.containerEnv) - && Objects.equals(containerMemoryLimit, that.containerMemoryLimit); - } + /** + * The WebClient timeout. + * Use this property to configure how long an HTTP client used by OIDC dev service admin client will wait + * for a response from OpenId Connect Provider when acquiring admin token and creating realm. + */ + @WithDefault("4S") + Duration webClientTimeout(); - @Override - public int hashCode() { - return Objects.hash(enabled, imageName, port, realmPath, realmName, users, roles, containerEnv, containerMemoryLimit); - } } diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesConfigBuildItem.java b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfigBuildItem.java similarity index 89% rename from extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesConfigBuildItem.java rename to extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfigBuildItem.java index efddc9b63ae8f..f8c0d8f1186a5 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesConfigBuildItem.java +++ b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfigBuildItem.java @@ -1,4 +1,4 @@ -package io.quarkus.oidc.deployment.devservices.keycloak; +package io.quarkus.devservices.keycloak; import java.util.Map; @@ -25,7 +25,7 @@ public Map getConfig() { return config; } - boolean isContainerRestarted() { + public boolean isContainerRestarted() { return containerRestarted; } } diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java similarity index 88% rename from extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java rename to extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java index 8bee58a01997a..9ad6cf05fb51e 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevServicesProcessor.java +++ b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java @@ -1,4 +1,9 @@ -package io.quarkus.oidc.deployment.devservices.keycloak; +package io.quarkus.devservices.keycloak; + +import static io.quarkus.devservices.keycloak.KeycloakDevServicesRequiredBuildItem.setOidcClientConfigProperties; +import static io.quarkus.devservices.keycloak.KeycloakDevServicesRequiredBuildItem.setOidcConfigProperties; +import static io.quarkus.devservices.keycloak.KeycloakDevServicesUtils.createWebClient; +import static io.quarkus.devservices.keycloak.KeycloakDevServicesUtils.getPasswordAccessToken; import java.io.IOException; import java.io.InputStream; @@ -59,10 +64,6 @@ import io.quarkus.devservices.common.ConfigureUtil; import io.quarkus.devservices.common.ContainerAddress; import io.quarkus.devservices.common.ContainerLocator; -import io.quarkus.oidc.deployment.OidcBuildStep.IsEnabled; -import io.quarkus.oidc.deployment.OidcBuildTimeConfig; -import io.quarkus.oidc.deployment.devservices.OidcDevServicesBuildItem; -import io.quarkus.oidc.runtime.devui.OidcDevServicesUtils; import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.configuration.ConfigUtils; import io.quarkus.runtime.configuration.MemorySize; @@ -74,7 +75,7 @@ import io.vertx.mutiny.ext.web.client.HttpResponse; import io.vertx.mutiny.ext.web.client.WebClient; -@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = { IsEnabled.class, GlobalDevServicesConfig.Enabled.class }) +@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class) public class KeycloakDevServicesProcessor { static volatile Vertx vertxInstance; @@ -91,6 +92,12 @@ public class KeycloakDevServicesProcessor { private static final String CLIENT_SECRET_CONFIG_KEY = CONFIG_PREFIX + "credentials.secret"; private static final String KEYCLOAK_URL_KEY = "keycloak.url"; + // OIDC Client config properties + static final String OIDC_CLIENT_AUTH_SERVER_URL_CONFIG_KEY = "quarkus.oidc-client.auth-server-url"; + static final String OIDC_CLIENT_TOKEN_PATH_CONFIG_KEY = "quarkus.oidc-client.token-path"; + private static final String OIDC_CLIENT_SECRET_CONFIG_KEY = "quarkus.oidc-client.credentials.secret"; + private static final String OIDC_CLIENT_ID_CONFIG_KEY = "quarkus.oidc-client.client-id"; + private static final String KEYCLOAK_CONTAINER_NAME = "keycloak"; private static final int KEYCLOAK_PORT = 8080; private static final int KEYCLOAK_HTTPS_PORT = 8443; @@ -127,38 +134,40 @@ public class KeycloakDevServicesProcessor { KEYCLOAK_PORT); private static volatile RunningDevService devService; - static volatile DevServicesConfig capturedDevServicesConfiguration; + static volatile KeycloakDevServicesConfig capturedDevServicesConfiguration; private static volatile boolean first = true; private static volatile Set capturedRealmFileLastModifiedDate; - - OidcBuildTimeConfig oidcConfig; + private static volatile boolean setOidcConfigProperties = true; + private static volatile boolean setOidcClientConfigProperties = true; @BuildStep public DevServicesResultBuildItem startKeycloakContainer( + List devSvcRequiredMarkerItems, DockerStatusBuildItem dockerStatusBuildItem, BuildProducer keycloakBuildItemBuildProducer, List devServicesSharedNetworkBuildItem, - Optional oidcProviderBuildItem, - KeycloakBuildTimeConfig config, + KeycloakDevServicesConfig config, CuratedApplicationShutdownBuildItem closeBuildItem, LaunchModeBuildItem launchMode, Optional consoleInstalledBuildItem, LoggingSetupBuildItem loggingSetupBuildItem, GlobalDevServicesConfig devServicesConfig) { - if (oidcProviderBuildItem.isPresent()) { - // Dev Services for the alternative OIDC provider are enabled + if (devSvcRequiredMarkerItems.isEmpty()) { return null; } - DevServicesConfig currentDevServicesConfiguration = config.devservices; + setOidcConfigProperties = setOidcConfigProperties(devSvcRequiredMarkerItems); + setOidcClientConfigProperties = setOidcClientConfigProperties(devSvcRequiredMarkerItems); + + KeycloakDevServicesConfig currentDevServicesConfiguration = config; // Figure out if we need to shut down and restart any existing Keycloak container // if not and the Keycloak container has already started we just return if (devService != null) { boolean restartRequired = !currentDevServicesConfiguration.equals(capturedDevServicesConfiguration); if (!restartRequired) { Set currentRealmFileLastModifiedDate = getRealmFileLastModifiedDate( - currentDevServicesConfiguration.realmPath); + currentDevServicesConfiguration.realmPath()); if (currentRealmFileLastModifiedDate != null && !currentRealmFileLastModifiedDate.equals(capturedRealmFileLastModifiedDate)) { restartRequired = true; @@ -240,7 +249,7 @@ public void run() { closeBuildItem.addCloseTask(closeTask, true); } - capturedRealmFileLastModifiedDate = getRealmFileLastModifiedDate(capturedDevServicesConfiguration.realmPath); + capturedRealmFileLastModifiedDate = getRealmFileLastModifiedDate(capturedDevServicesConfiguration.realmPath()); if (devService != null && errors.isEmpty()) { compressor.close(); } else { @@ -261,8 +270,7 @@ private String startURL(String scheme, String host, Integer port, boolean isKeyc private Map prepareConfiguration( BuildProducer keycloakBuildItemBuildProducer, String internalURL, - String hostURL, List realmReps, - boolean keycloakX, List errors) { + String hostURL, List realmReps, List errors) { final String realmName = realmReps != null && !realmReps.isEmpty() ? realmReps.iterator().next().getRealm() : getDefaultRealmName(); final String authServerInternalUrl = realmsURL(internalURL, realmName); @@ -270,13 +278,14 @@ private Map prepareConfiguration( String clientAuthServerBaseUrl = hostURL != null ? hostURL : internalURL; String clientAuthServerUrl = realmsURL(clientAuthServerBaseUrl, realmName); - boolean createDefaultRealm = (realmReps == null || realmReps.isEmpty()) && capturedDevServicesConfiguration.createRealm; + boolean createDefaultRealm = (realmReps == null || realmReps.isEmpty()) + && capturedDevServicesConfiguration.createRealm(); String oidcClientId = getOidcClientId(); String oidcClientSecret = getOidcClientSecret(); String oidcApplicationType = getOidcApplicationType(); - Map users = getUsers(capturedDevServicesConfiguration.users, createDefaultRealm); + Map users = getUsers(capturedDevServicesConfiguration.users(), createDefaultRealm); List realmNames = new LinkedList<>(); @@ -286,7 +295,7 @@ private Map prepareConfiguration( vertxInstance = Vertx.vertx(); } - WebClient client = OidcDevServicesUtils.createWebClient(vertxInstance); + WebClient client = createWebClient(vertxInstance); try { String adminToken = getAdminToken(client, clientAuthServerBaseUrl); if (createDefaultRealm) { @@ -306,12 +315,24 @@ private Map prepareConfiguration( Map configProperties = new HashMap<>(); configProperties.put(KEYCLOAK_URL_KEY, internalURL); - configProperties.put(AUTH_SERVER_URL_CONFIG_KEY, authServerInternalUrl); configProperties.put(CLIENT_AUTH_SERVER_URL_CONFIG_KEY, clientAuthServerUrl); - configProperties.put(APPLICATION_TYPE_CONFIG_KEY, oidcApplicationType); - if (capturedDevServicesConfiguration.createClient) { - configProperties.put(CLIENT_ID_CONFIG_KEY, oidcClientId); - configProperties.put(CLIENT_SECRET_CONFIG_KEY, oidcClientSecret); + if (setOidcClientConfigProperties) { + configProperties.put(OIDC_CLIENT_AUTH_SERVER_URL_CONFIG_KEY, authServerInternalUrl); + configProperties.put(OIDC_CLIENT_TOKEN_PATH_CONFIG_KEY, "/protocol/openid-connect/tokens"); + } + if (setOidcConfigProperties) { + configProperties.put(AUTH_SERVER_URL_CONFIG_KEY, authServerInternalUrl); + configProperties.put(APPLICATION_TYPE_CONFIG_KEY, oidcApplicationType); + } + if (capturedDevServicesConfiguration.createClient()) { + if (setOidcConfigProperties) { + configProperties.put(CLIENT_ID_CONFIG_KEY, oidcClientId); + configProperties.put(CLIENT_SECRET_CONFIG_KEY, oidcClientSecret); + } + if (setOidcClientConfigProperties) { + configProperties.put(OIDC_CLIENT_ID_CONFIG_KEY, oidcClientId); + configProperties.put(OIDC_CLIENT_SECRET_CONFIG_KEY, oidcClientSecret); + } } configProperties.put(OIDC_USERS, users.entrySet().stream() .map(e -> e.toString()).collect(Collectors.joining(","))); @@ -329,19 +350,19 @@ private String realmsURL(String baseURL, String realmName) { } private String getDefaultRealmName() { - return capturedDevServicesConfiguration.realmName.orElse("quarkus"); + return capturedDevServicesConfiguration.realmName().orElse("quarkus"); } private RunningDevService startContainer(DockerStatusBuildItem dockerStatusBuildItem, BuildProducer keycloakBuildItemBuildProducer, boolean useSharedNetwork, Optional timeout, List errors) { - if (!capturedDevServicesConfiguration.enabled) { + if (!capturedDevServicesConfiguration.enabled()) { // explicitly disabled LOG.debug("Not starting Dev Services for Keycloak as it has been disabled in the config"); return null; } - if (!isOidcTenantEnabled() && !capturedDevServicesConfiguration.startWithDisabledTenant) { + if (!isOidcTenantEnabled() && !capturedDevServicesConfiguration.startWithDisabledTenant()) { LOG.debug("Not starting Dev Services for Keycloak as 'quarkus.oidc.tenant.enabled' is false"); return null; } @@ -359,31 +380,46 @@ private RunningDevService startContainer(DockerStatusBuildItem dockerStatusBuild return null; } + // TODO: this will need to be reworked when we integrate with other extensions like Keycloak Admin Client + if (!setOidcConfigProperties && !setOidcClientConfigProperties) { + // this can happen if OIDC is not present or disabled and user set either of following properties: + if (ConfigUtils.isPropertyNonEmpty(OIDC_CLIENT_AUTH_SERVER_URL_CONFIG_KEY)) { + LOG.debugf("Not starting Dev Services for Keycloak as '%s' has been provided", + OIDC_CLIENT_AUTH_SERVER_URL_CONFIG_KEY); + return null; + } + if (ConfigUtils.isPropertyNonEmpty(OIDC_CLIENT_TOKEN_PATH_CONFIG_KEY)) { + LOG.debugf("Not starting Dev Services for Keycloak as '%s' has been provided", + OIDC_CLIENT_TOKEN_PATH_CONFIG_KEY); + return null; + } + } + final Optional maybeContainerAddress = keycloakDevModeContainerLocator.locateContainer( - capturedDevServicesConfiguration.serviceName, - capturedDevServicesConfiguration.shared, + capturedDevServicesConfiguration.serviceName(), + capturedDevServicesConfiguration.shared(), LaunchMode.current()); - String imageName = capturedDevServicesConfiguration.imageName; + String imageName = capturedDevServicesConfiguration.imageName(); DockerImageName dockerImageName = DockerImageName.parse(imageName).asCompatibleSubstituteFor(imageName); final Supplier defaultKeycloakContainerSupplier = () -> { QuarkusOidcContainer oidcContainer = new QuarkusOidcContainer(dockerImageName, - capturedDevServicesConfiguration.port, + capturedDevServicesConfiguration.port(), useSharedNetwork, - capturedDevServicesConfiguration.realmPath.orElse(List.of()), + capturedDevServicesConfiguration.realmPath().orElse(List.of()), resourcesMap(errors), - capturedDevServicesConfiguration.serviceName, - capturedDevServicesConfiguration.shared, - capturedDevServicesConfiguration.javaOpts, - capturedDevServicesConfiguration.startCommand, - capturedDevServicesConfiguration.showLogs, - capturedDevServicesConfiguration.containerMemoryLimit, + capturedDevServicesConfiguration.serviceName(), + capturedDevServicesConfiguration.shared(), + capturedDevServicesConfiguration.javaOpts(), + capturedDevServicesConfiguration.startCommand(), + capturedDevServicesConfiguration.showLogs(), + capturedDevServicesConfiguration.containerMemoryLimit(), errors); timeout.ifPresent(oidcContainer::withStartupTimeout); - oidcContainer.withEnv(capturedDevServicesConfiguration.containerEnv); + oidcContainer.withEnv(capturedDevServicesConfiguration.containerEnv()); oidcContainer.start(); String internalUrl = startURL((oidcContainer.isHttps() ? "https://" : "http://"), oidcContainer.getHost(), @@ -396,9 +432,7 @@ private RunningDevService startContainer(DockerStatusBuildItem dockerStatusBuild : null; Map configs = prepareConfiguration(keycloakBuildItemBuildProducer, internalUrl, hostUrl, - oidcContainer.realmReps, - oidcContainer.keycloakX, - errors); + oidcContainer.realmReps, errors); return new RunningDevService(KEYCLOAK_CONTAINER_NAME, oidcContainer.getContainerId(), oidcContainer::close, configs); }; @@ -408,7 +442,7 @@ private RunningDevService startContainer(DockerStatusBuildItem dockerStatusBuild // TODO: this probably needs to be addressed Map configs = prepareConfiguration(keycloakBuildItemBuildProducer, getSharedContainerUrl(containerAddress), - getSharedContainerUrl(containerAddress), null, false, errors); + getSharedContainerUrl(containerAddress), null, errors); return new RunningDevService(KEYCLOAK_CONTAINER_NAME, containerAddress.getId(), null, configs); }) .orElseGet(defaultKeycloakContainerSupplier); @@ -416,10 +450,10 @@ private RunningDevService startContainer(DockerStatusBuildItem dockerStatusBuild private Map resourcesMap(List errors) { Map resources = new HashMap<>(); - for (Map.Entry aliasEntry : capturedDevServicesConfiguration.resourceAliases.entrySet()) { - if (capturedDevServicesConfiguration.resourceMappings.containsKey(aliasEntry.getKey())) { + for (Map.Entry aliasEntry : capturedDevServicesConfiguration.resourceAliases().entrySet()) { + if (capturedDevServicesConfiguration.resourceMappings().containsKey(aliasEntry.getKey())) { resources.put(aliasEntry.getValue(), - capturedDevServicesConfiguration.resourceMappings.get(aliasEntry.getKey())); + capturedDevServicesConfiguration.resourceMappings().get(aliasEntry.getKey())); } else { errors.add(String.format("%s alias for the %s resource does not have a mapping", aliasEntry.getKey(), aliasEntry.getValue())); @@ -431,8 +465,8 @@ private Map resourcesMap(List errors) { } private static boolean isKeycloakX(DockerImageName dockerImageName) { - return capturedDevServicesConfiguration.keycloakXImage.isPresent() - ? capturedDevServicesConfiguration.keycloakXImage.get() + return capturedDevServicesConfiguration.keycloakXImage().isPresent() + ? capturedDevServicesConfiguration.keycloakXImage().get() : !dockerImageName.getVersionPart().endsWith(KEYCLOAK_LEGACY_IMAGE_VERSION_PART); } @@ -688,7 +722,7 @@ private void createDefaultRealm(WebClient client, String token, String keycloakU List errors) { RealmRepresentation realm = createDefaultRealmRep(); - if (capturedDevServicesConfiguration.createClient) { + if (capturedDevServicesConfiguration.createClient()) { realm.getClients().add(createClient(oidcClientId, oidcClientSecret)); } for (Map.Entry entry : users.entrySet()) { @@ -702,10 +736,10 @@ private String getAdminToken(WebClient client, String keycloakUrl) { try { LOG.tracef("Acquiring admin token"); - return OidcDevServicesUtils.getPasswordAccessToken(client, + return getPasswordAccessToken(client, keycloakUrl + "/realms/master/protocol/openid-connect/token", "admin-cli", null, "admin", "admin", null) - .await().atMost(oidcConfig.devui.webClientTimeout); + .await().atMost(capturedDevServicesConfiguration.webClientTimeout()); } catch (TimeoutException e) { LOG.error("Admin token can not be acquired due to a client connection timeout. " + "You may try increasing the `quarkus.oidc.devui.web-client-timeout` property."); @@ -723,7 +757,7 @@ private void createRealm(WebClient client, String token, String keycloakUrl, Rea .putHeader(HttpHeaders.CONTENT_TYPE.toString(), "application/json") .putHeader(HttpHeaders.AUTHORIZATION.toString(), "Bearer " + token) .sendBuffer(Buffer.buffer().appendString(JsonSerialization.writeValueAsString(realm))) - .await().atMost(oidcConfig.devui.webClientTimeout); + .await().atMost(capturedDevServicesConfiguration.webClientTimeout()); if (createRealmResponse.statusCode() > 299) { errors.add(String.format("Realm %s can not be created %d - %s ", realm.getRealm(), @@ -790,7 +824,7 @@ private Map getUsers(Map configuredUsers, boolea } private List getUserRoles(String user) { - List roles = capturedDevServicesConfiguration.roles.get(user); + List roles = capturedDevServicesConfiguration.roles().get(user); return roles == null ? ("alice".equals(user) ? List.of("admin", "user") : List.of("user")) : roles; } @@ -812,12 +846,12 @@ private RealmRepresentation createDefaultRealmRep() { roles.setRealm(realmRoles); realm.setRoles(roles); - if (capturedDevServicesConfiguration.roles.isEmpty()) { + if (capturedDevServicesConfiguration.roles().isEmpty()) { realm.getRoles().getRealm().add(new RoleRepresentation("user", null, false)); realm.getRoles().getRealm().add(new RoleRepresentation("admin", null, false)); } else { Set allRoles = new HashSet<>(); - for (List distinctRoles : capturedDevServicesConfiguration.roles.values()) { + for (List distinctRoles : capturedDevServicesConfiguration.roles().values()) { for (String role : distinctRoles) { if (!allRoles.contains(role)) { allRoles.add(role); @@ -876,12 +910,12 @@ private static String getOidcApplicationType() { private static String getOidcClientId() { // if the application type is web-app or hybrid, OidcRecorder will enforce that the client id and secret are configured return ConfigProvider.getConfig().getOptionalValue(CLIENT_ID_CONFIG_KEY, String.class) - .orElse(capturedDevServicesConfiguration.createClient ? "quarkus-app" : ""); + .orElse(capturedDevServicesConfiguration.createClient() ? "quarkus-app" : ""); } private static String getOidcClientSecret() { // if the application type is web-app or hybrid, OidcRecorder will enforce that the client id and secret are configured return ConfigProvider.getConfig().getOptionalValue(CLIENT_SECRET_CONFIG_KEY, String.class) - .orElse(capturedDevServicesConfiguration.createClient ? "secret" : ""); + .orElse(capturedDevServicesConfiguration.createClient() ? "secret" : ""); } } diff --git a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesRequiredBuildItem.java b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesRequiredBuildItem.java new file mode 100644 index 0000000000000..8bdeff81b335b --- /dev/null +++ b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesRequiredBuildItem.java @@ -0,0 +1,47 @@ +package io.quarkus.devservices.keycloak; + +import static io.quarkus.devservices.keycloak.KeycloakDevServicesProcessor.OIDC_CLIENT_AUTH_SERVER_URL_CONFIG_KEY; +import static io.quarkus.devservices.keycloak.KeycloakDevServicesProcessor.OIDC_CLIENT_TOKEN_PATH_CONFIG_KEY; + +import java.util.List; + +import io.quarkus.builder.item.MultiBuildItem; +import io.quarkus.runtime.configuration.ConfigUtils; + +/** + * A marker build item signifying that integrating extensions (like OIDC and OIDC client) + * are enabled. The Keycloak Dev Service will be started in DEV mode if at least one item is produced + * and the Dev Service is not disabled in other fashion. + */ +public final class KeycloakDevServicesRequiredBuildItem extends MultiBuildItem { + + enum Capability { + OIDC, + OIDC_CLIENT + } + + private final Capability capability; + + private KeycloakDevServicesRequiredBuildItem(Capability capability) { + this.capability = capability; + } + + static boolean setOidcConfigProperties(List items) { + return items.stream().anyMatch(i -> i.capability == Capability.OIDC); + } + + static boolean setOidcClientConfigProperties(List items) { + boolean serverUrlOrTokenPathConfigured = ConfigUtils.isPropertyNonEmpty(OIDC_CLIENT_AUTH_SERVER_URL_CONFIG_KEY) + || ConfigUtils.isPropertyNonEmpty(OIDC_CLIENT_TOKEN_PATH_CONFIG_KEY); + return !serverUrlOrTokenPathConfigured + && items.stream().anyMatch(i -> i.capability == Capability.OIDC_CLIENT); + } + + public static KeycloakDevServicesRequiredBuildItem requireDevServiceForOidc() { + return new KeycloakDevServicesRequiredBuildItem(Capability.OIDC); + } + + public static KeycloakDevServicesRequiredBuildItem requireDevServiceForOidcClient() { + return new KeycloakDevServicesRequiredBuildItem(Capability.OIDC_CLIENT); + } +} diff --git a/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesUtils.java b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesUtils.java new file mode 100644 index 0000000000000..53eaffce8578b --- /dev/null +++ b/extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesUtils.java @@ -0,0 +1,96 @@ +package io.quarkus.devservices.keycloak; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Map; + +import io.smallrye.mutiny.Uni; +import io.vertx.core.MultiMap; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.client.WebClientOptions; +import io.vertx.mutiny.core.buffer.Buffer; +import io.vertx.mutiny.ext.web.client.HttpRequest; +import io.vertx.mutiny.ext.web.client.HttpResponse; +import io.vertx.mutiny.ext.web.client.WebClient; + +final class KeycloakDevServicesUtils { + + private static final byte AMP = '&'; + private static final byte EQ = '='; + + private KeycloakDevServicesUtils() { + + } + + static WebClient createWebClient(Vertx vertx) { + WebClientOptions options = new WebClientOptions(); + options.setTrustAll(true); + options.setVerifyHost(false); + return WebClient.create(new io.vertx.mutiny.core.Vertx(vertx), options); + } + + static Uni getPasswordAccessToken(WebClient client, + String tokenUrl, + String clientId, + String clientSecret, + String userName, + String userPassword, + Map passwordGrantOptions) { + HttpRequest request = client.postAbs(tokenUrl); + request.putHeader(HttpHeaders.CONTENT_TYPE.toString(), HttpHeaders.APPLICATION_X_WWW_FORM_URLENCODED.toString()); + + io.vertx.mutiny.core.MultiMap props = new io.vertx.mutiny.core.MultiMap(MultiMap.caseInsensitiveMultiMap()); + props.add("client_id", clientId); + if (clientSecret != null) { + props.add("client_secret", clientSecret); + } + + props.add("username", userName); + props.add("password", userPassword); + props.add("grant_type", "password"); + if (passwordGrantOptions != null) { + props.addAll(passwordGrantOptions); + } + + return request.sendBuffer(encodeForm(props)).onItem() + .transform(KeycloakDevServicesUtils::getAccessTokenFromJson) + .onFailure() + .retry() + .withBackOff(Duration.ofSeconds(2), Duration.ofSeconds(2)) + .expireIn(10 * 1000); + } + + private static String getAccessTokenFromJson(HttpResponse resp) { + if (resp.statusCode() == 200) { + JsonObject json = resp.bodyAsJsonObject(); + return json.getString("access_token"); + } else { + String errorMessage = resp.bodyAsString(); + throw new RuntimeException(errorMessage); + } + } + + private static Buffer encodeForm(io.vertx.mutiny.core.MultiMap form) { + Buffer buffer = Buffer.buffer(); + for (Map.Entry entry : form) { + if (buffer.length() != 0) { + buffer.appendByte(AMP); + } + buffer.appendString(entry.getKey()); + buffer.appendByte(EQ); + buffer.appendString(urlEncode(entry.getValue())); + } + return buffer; + } + + private static String urlEncode(String value) { + try { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/extensions/oidc/deployment/src/main/resources/dev-service/upconfig.json b/extensions/devservices/keycloak/src/main/resources/dev-service/upconfig.json similarity index 100% rename from extensions/oidc/deployment/src/main/resources/dev-service/upconfig.json rename to extensions/devservices/keycloak/src/main/resources/dev-service/upconfig.json diff --git a/extensions/devservices/pom.xml b/extensions/devservices/pom.xml index 84d2af8ab037e..5f0851f718f7a 100644 --- a/extensions/devservices/pom.xml +++ b/extensions/devservices/pom.xml @@ -28,6 +28,7 @@ oracle common deployment + keycloak diff --git a/extensions/oidc-client/deployment/pom.xml b/extensions/oidc-client/deployment/pom.xml index 69e68bbdd88dd..3e96bb92c13c7 100644 --- a/extensions/oidc-client/deployment/pom.xml +++ b/extensions/oidc-client/deployment/pom.xml @@ -35,7 +35,7 @@
io.quarkus - quarkus-devservices-deployment + quarkus-devservices-keycloak diff --git a/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientBuildStep.java b/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientBuildStep.java index fe6e9fae6f74f..0dd562bec6fc4 100644 --- a/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientBuildStep.java +++ b/extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientBuildStep.java @@ -25,6 +25,8 @@ import io.quarkus.arc.processor.DotNames; import io.quarkus.deployment.ApplicationArchive; import io.quarkus.deployment.Feature; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; @@ -35,6 +37,11 @@ import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; +import io.quarkus.devservices.keycloak.KeycloakDevServicesConfigBuildItem; +import io.quarkus.devservices.keycloak.KeycloakDevServicesRequiredBuildItem; +import io.quarkus.devui.spi.page.CardPageBuildItem; +import io.quarkus.devui.spi.page.Page; import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.ClassOutput; import io.quarkus.gizmo.MethodCreator; @@ -184,6 +191,27 @@ private AccessTokenInstanceBuildItem build() { return index.getIndex().getAnnotations(ACCESS_TOKEN).stream().map(ItemBuilder::new).map(ItemBuilder::build).toList(); } + @BuildStep(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class) + KeycloakDevServicesRequiredBuildItem requireKeycloakDevService() { + // this needs to be done as the shared Keycloak Dev Service doesn't know if the OIDC Client is enabled + return KeycloakDevServicesRequiredBuildItem.requireDevServiceForOidcClient(); + } + + @BuildStep(onlyIf = IsDevelopment.class) + void produceDevUiCardWithKeycloakUrl(Optional configProps, + BuildProducer cardPageProducer) { + final String keycloakAdminUrl = configProps.map(item -> item.getConfig().get("keycloak.url")).orElse(null); + if (keycloakAdminUrl != null) { + // Add Admin page + final CardPageBuildItem cardPage = new CardPageBuildItem(); + cardPage.addPage(Page.externalPageBuilder("Keycloak Admin") + .icon("font-awesome-solid:key") + .doNotEmbed(true) + .url(keycloakAdminUrl)); + cardPageProducer.produce(cardPage); + } + } + /** * Creates a Tokens producer class like follows: * diff --git a/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientKeycloakDevServiceTest.java b/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientKeycloakDevServiceTest.java new file mode 100644 index 0000000000000..b5f64ce0343ad --- /dev/null +++ b/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientKeycloakDevServiceTest.java @@ -0,0 +1,63 @@ +package io.quarkus.oidc.client; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.oidc.runtime.OidcUtils; +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +/** + * Test Keycloak Dev Service is started when OIDC extension is disabled (or not present, though indirectly). + * OIDC client auth server URL and client id and secret must be automatically configured for this test to pass. + */ +public class OidcClientKeycloakDevServiceTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(NamedOidcClientResource.class) + .addAsResource("oidc-client-dev-service-test.properties", "application.properties")); + + @Test + public void testInjectedNamedOidcClients() { + String token1 = doTestGetTokenByNamedClient("client1"); + String token2 = doTestGetTokenByNamedClient("client2"); + validateTokens(token1, token2); + } + + @Test + public void testInjectedNamedTokens() { + String token1 = doTestGetTokenByNamedTokensProvider("client1"); + String token2 = doTestGetTokenByNamedTokensProvider("client2"); + validateTokens(token1, token2); + } + + private void validateTokens(String token1, String token2) { + assertThat(token1, is(not(equalTo(token2)))); + assertThat(upn(token1), is("alice")); + assertThat(upn(token2), is("bob")); + } + + private String upn(String token) { + return OidcUtils.decodeJwtContent(token).getString("upn"); + } + + private String doTestGetTokenByNamedClient(String clientId) { + String token = RestAssured.given().get("/" + clientId + "/token").body().asString(); + assertThat(token, is(notNullValue())); + return token; + } + + private String doTestGetTokenByNamedTokensProvider(String clientId) { + String token = RestAssured.given().get("/" + clientId + "/token/singleton").body().asString(); + assertThat(token, is(notNullValue())); + return token; + } +} diff --git a/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientPasswordGrantSecretIsMissingTestCase.java b/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientPasswordGrantSecretIsMissingTestCase.java index 0e1d80f22152c..5c19632a52699 100644 --- a/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientPasswordGrantSecretIsMissingTestCase.java +++ b/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientPasswordGrantSecretIsMissingTestCase.java @@ -33,7 +33,7 @@ public class OidcClientPasswordGrantSecretIsMissingTestCase { } e = e.getCause(); } - assertNotNull(te); + assertNotNull(te, "Expected ConfigurationException, but got: " + t); assertTrue( te.getMessage() .contains("Username and password must be set when a password grant is used"), diff --git a/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientTooManyJwtCredentialKeyPropsTestCase.java b/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientTooManyJwtCredentialKeyPropsTestCase.java index a4eae56be1217..cfa4397a49db7 100644 --- a/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientTooManyJwtCredentialKeyPropsTestCase.java +++ b/extensions/oidc-client/deployment/src/test/java/io/quarkus/oidc/client/OidcClientTooManyJwtCredentialKeyPropsTestCase.java @@ -33,7 +33,7 @@ public class OidcClientTooManyJwtCredentialKeyPropsTestCase { } e = e.getCause(); } - assertNotNull(te); + assertNotNull(te, "Expected ConfigurationException, but got: " + t); assertEquals( "Only a single OIDC JWT credential key property can be configured, but you have configured:" + " quarkus.oidc-client.credentials.jwt.key,quarkus.oidc-client.credentials.jwt.secret", diff --git a/extensions/oidc-client/deployment/src/test/resources/oidc-client-dev-service-test.properties b/extensions/oidc-client/deployment/src/test/resources/oidc-client-dev-service-test.properties new file mode 100644 index 0000000000000..51063eea9940c --- /dev/null +++ b/extensions/oidc-client/deployment/src/test/resources/oidc-client-dev-service-test.properties @@ -0,0 +1,15 @@ +quarkus.oidc.enabled=false + +quarkus.oidc-client.client1.auth-server-url=${quarkus.oidc-client.auth-server-url} +quarkus.oidc-client.client1.client-id=${quarkus.oidc-client.client-id} +quarkus.oidc-client.client1.credentials.secret=${quarkus.oidc-client.credentials.secret} +quarkus.oidc-client.client1.grant.type=password +quarkus.oidc-client.client1.grant-options.password.username=alice +quarkus.oidc-client.client1.grant-options.password.password=alice + +quarkus.oidc-client.client2.auth-server-url=${quarkus.oidc-client.auth-server-url} +quarkus.oidc-client.client2.client-id=${quarkus.oidc-client.client-id} +quarkus.oidc-client.client2.credentials.secret=${quarkus.oidc-client.credentials.secret} +quarkus.oidc-client.client2.grant.type=password +quarkus.oidc-client.client2.grant-options.password.username=bob +quarkus.oidc-client.client2.grant-options.password.password=bob diff --git a/extensions/oidc-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/oidc-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml index 434ccf025932b..3282e98c0a163 100644 --- a/extensions/oidc-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/oidc-client/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -14,3 +14,4 @@ metadata: config: - "quarkus.oidc-client." - "quarkus.oidc." + - "quarkus.keycloak.devservices." diff --git a/extensions/oidc/deployment/pom.xml b/extensions/oidc/deployment/pom.xml index d7d217b5cb6c6..8e45ae8a28f43 100644 --- a/extensions/oidc/deployment/pom.xml +++ b/extensions/oidc/deployment/pom.xml @@ -37,46 +37,34 @@ io.quarkus quarkus-security-deployment - - io.quarkus - quarkus-devservices-deployment - io.quarkus quarkus-jsonp-deployment - - org.keycloak - keycloak-core - - - com.sun.activation - jakarta.activation - - - org.eclipse.angus angus-activation - org.testcontainers - testcontainers - - - junit - junit - - + io.quarkus + quarkus-junit4-mock + test io.quarkus - quarkus-junit4-mock + quarkus-devservices-keycloak io.quarkus quarkus-test-keycloak-server + + + + junit + junit + + test diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/DevUiConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/DevUiConfig.java index b16c3aebaa324..aca96f6e25a7d 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/DevUiConfig.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/DevUiConfig.java @@ -72,7 +72,6 @@ public String getGrantType() { * The WebClient timeout. * Use this property to configure how long an HTTP client used by Dev UI handlers will wait for a response when requesting * tokens from OpenId Connect Provider and sending them to the service endpoint. - * This timeout is also used by the OIDC dev service admin client. */ @ConfigItem(defaultValue = "4S") public Duration webClientTimeout; diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java index c5b4ccea69d6b..d173468d4383f 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/OidcBuildStep.java @@ -46,6 +46,7 @@ import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; import io.quarkus.deployment.Feature; +import io.quarkus.deployment.IsNormal; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.BuildSteps; @@ -57,6 +58,8 @@ import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem; import io.quarkus.deployment.builditem.SystemPropertyBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig; +import io.quarkus.devservices.keycloak.KeycloakDevServicesRequiredBuildItem; import io.quarkus.oidc.AuthorizationCodeFlow; import io.quarkus.oidc.BearerTokenAuthentication; import io.quarkus.oidc.IdToken; @@ -395,6 +398,12 @@ List registerHttpAuthMechanismAnnotation() new HttpAuthMechanismAnnotationBuildItem(DotName.createSimple(BearerTokenAuthentication.class), BEARER_SCHEME)); } + @BuildStep(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class) + KeycloakDevServicesRequiredBuildItem requireKeycloakDevService() { + // this needs to be done as the shared Keycloak Dev Service doesn't know if the OIDC is enabled + return KeycloakDevServicesRequiredBuildItem.requireDevServiceForOidc(); + } + private static boolean isInjected(BeanRegistrationPhaseBuildItem beanRegistrationPhaseBuildItem, DotName requiredType, DotName withoutQualifier) { for (InjectionPointInfo injectionPoint : beanRegistrationPhaseBuildItem.getInjectionPoints()) { diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/AbstractDevUIProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/AbstractDevUIProcessor.java index 890c86f22a6b3..5f4b749c4eae1 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/AbstractDevUIProcessor.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/AbstractDevUIProcessor.java @@ -20,12 +20,6 @@ public abstract class AbstractDevUIProcessor { protected static final String CONFIG_PREFIX = "quarkus.oidc."; protected static final String CLIENT_ID_CONFIG_KEY = CONFIG_PREFIX + "client-id"; - protected static final String CLIENT_SECRET_CONFIG_KEY = CONFIG_PREFIX + "credentials.secret"; - protected static final String AUTHORIZATION_PATH_CONFIG_KEY = CONFIG_PREFIX + "authorization-path"; - protected static final String TOKEN_PATH_CONFIG_KEY = CONFIG_PREFIX + "token-path"; - protected static final String END_SESSION_PATH_CONFIG_KEY = CONFIG_PREFIX + "end-session-path"; - protected static final String POST_LOGOUT_URI_PARAM_CONFIG_KEY = CONFIG_PREFIX + "logout.post-logout-uri-param"; - protected static final String SCOPES_KEY = CONFIG_PREFIX + "authentication.scopes"; protected static CardPageBuildItem createProviderWebComponent(OidcDevUiRecorder recorder, Capabilities capabilities, diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevServicesBuildItem.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevServicesBuildItem.java deleted file mode 100644 index 6fafbb817a38c..0000000000000 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/OidcDevServicesBuildItem.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.quarkus.oidc.deployment.devservices; - -import io.quarkus.builder.item.SimpleBuildItem; - -/** - * Marker build item which indicates that Dev Services for OIDC are provided by another extension. - * Dev Services for Keycloak will be disabled if this item is detected. - */ -public class OidcDevServicesBuildItem extends SimpleBuildItem { - -} diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakBuildTimeConfig.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakBuildTimeConfig.java deleted file mode 100644 index d30df58effb70..0000000000000 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakBuildTimeConfig.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.quarkus.oidc.deployment.devservices.keycloak; - -import io.quarkus.runtime.annotations.ConfigDocSection; -import io.quarkus.runtime.annotations.ConfigItem; -import io.quarkus.runtime.annotations.ConfigRoot; - -/** - * Build time configuration for OIDC. - */ -@ConfigRoot -public class KeycloakBuildTimeConfig { - /** - * Dev Services. - */ - @ConfigItem - @ConfigDocSection(generated = true) - public DevServicesConfig devservices; -} diff --git a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevUIProcessor.java b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevUIProcessor.java index 1127d02c099a6..2cd325f68eadb 100644 --- a/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevUIProcessor.java +++ b/extensions/oidc/deployment/src/main/java/io/quarkus/oidc/deployment/devservices/keycloak/KeycloakDevUIProcessor.java @@ -14,9 +14,11 @@ import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.ConfigurationBuildItem; import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem; +import io.quarkus.devservices.keycloak.KeycloakDevServicesConfigBuildItem; import io.quarkus.devui.spi.JsonRPCProvidersBuildItem; import io.quarkus.devui.spi.page.CardPageBuildItem; import io.quarkus.devui.spi.page.Page; +import io.quarkus.oidc.deployment.DevUiConfig; import io.quarkus.oidc.deployment.OidcBuildTimeConfig; import io.quarkus.oidc.deployment.devservices.AbstractDevUIProcessor; import io.quarkus.oidc.runtime.devui.OidcDevJsonRpcService; @@ -25,7 +27,6 @@ public class KeycloakDevUIProcessor extends AbstractDevUIProcessor { - KeycloakBuildTimeConfig keycloakConfig; OidcBuildTimeConfig oidcConfig; @Record(ExecutionTime.RUNTIME_INIT) @@ -53,8 +54,7 @@ void produceProviderComponent(Optional confi capabilities, "Keycloak", configProps.get().getConfig().get("quarkus.oidc.application-type"), - oidcConfig.devui.grant.type.isPresent() ? oidcConfig.devui.grant.type.get().getGrantType() - : keycloakConfig.devservices.grant.type.getGrantType(), + oidcConfig.devui.grant.type.orElse(DevUiConfig.Grant.Type.CODE).getGrantType(), realmUrl + "/protocol/openid-connect/auth", realmUrl + "/protocol/openid-connect/token", realmUrl + "/protocol/openid-connect/logout",