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 87%
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..afc9a54144ec0 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,13 @@ 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 OIDC_CLIENT_DISCOVERY_ENABLED_CONFIG_KEY = "quarkus.oidc-client.discovery-enabled";
+
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 +135,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 +250,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 +271,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 +279,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 +296,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 +316,27 @@ 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 (!ConfigUtils.isPropertyNonEmpty(OIDC_CLIENT_DISCOVERY_ENABLED_CONFIG_KEY)) {
+ configProperties.put(OIDC_CLIENT_DISCOVERY_ENABLED_CONFIG_KEY, "false");
+ }
+ }
+ 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 +354,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 +384,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 +436,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 +446,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 +454,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 +469,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 +726,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 +740,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 +761,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 +828,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 +850,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 +914,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 @@