Skip to content

Commit

Permalink
Support Keycloak Dev Services for standalone OIDC Client Registration
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Nov 2, 2024
1 parent 0fbfc31 commit 19c8c62
Show file tree
Hide file tree
Showing 14 changed files with 502 additions and 215 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.quarkus.devservices.keycloak;

import java.util.Comparator;
import java.util.List;

import io.quarkus.builder.item.MultiBuildItem;
import io.quarkus.devui.spi.page.CardPageBuildItem;

public final class KeycloakAdminPageBuildItem extends MultiBuildItem {

public static final int DEFAULT_PRIORITY = 0;

private final CardPageBuildItem cardPageBuildItem;
/**
* Determines which {@link #cardPageBuildItem} is selected.
* Higher value means higher priority.
*/
private final int priority;

public KeycloakAdminPageBuildItem(CardPageBuildItem cardPageBuildItem) {
this(cardPageBuildItem, DEFAULT_PRIORITY);
}

public KeycloakAdminPageBuildItem(CardPageBuildItem cardPageBuildItem, int priority) {
this.cardPageBuildItem = cardPageBuildItem;
this.priority = priority;
}

static CardPageBuildItem getCardPageBuildItem(List<KeycloakAdminPageBuildItem> items) {
return items
.stream()
.sorted(Comparator.<KeycloakAdminPageBuildItem> comparingInt(i -> i.priority).reversed())
.map(i -> i.cardPageBuildItem)
.findFirst()
.orElse(null);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package io.quarkus.devservices.keycloak;

import static io.quarkus.devservices.keycloak.KeycloakDevServicesProcessor.KEYCLOAK_URL_KEY;

import java.util.Map;
import java.util.Optional;

import io.quarkus.builder.item.SimpleBuildItem;

Expand Down Expand Up @@ -28,4 +31,11 @@ public Map<String, String> getConfig() {
public boolean isContainerRestarted() {
return containerRestarted;
}

public static String getKeycloakUrl(Optional<KeycloakDevServicesConfigBuildItem> configBuildItem) {
return configBuildItem
.map(KeycloakDevServicesConfigBuildItem::getConfig)
.map(config -> config.get(KEYCLOAK_URL_KEY))
.orElse(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.quarkus.devservices.keycloak;

import java.util.Map;

import org.keycloak.representations.idm.RealmRepresentation;

public interface KeycloakDevServicesConfigurator {

record ConfigPropertiesContext(String authServerInternalUrl, String oidcClientId, String oidcClientSecret) {
}

Map<String, String> createProperties(ConfigPropertiesContext context);

default void customizeDefaultRealm(RealmRepresentation realmRepresentation) {
}

}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
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 static java.util.Objects.requireNonNull;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.keycloak.representations.idm.RealmRepresentation;

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)
Expand All @@ -15,33 +18,33 @@
*/
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<KeycloakDevServicesRequiredBuildItem> items) {
return items.stream().anyMatch(i -> i.capability == Capability.OIDC);
}
private final KeycloakDevServicesConfigurator devServicesConfigurator;

static boolean setOidcClientConfigProperties(List<KeycloakDevServicesRequiredBuildItem> 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 KeycloakDevServicesRequiredBuildItem(KeycloakDevServicesConfigurator devServicesConfigurator) {
this.devServicesConfigurator = requireNonNull(devServicesConfigurator);
}

public static KeycloakDevServicesRequiredBuildItem requireDevServiceForOidc() {
return new KeycloakDevServicesRequiredBuildItem(Capability.OIDC);
static KeycloakDevServicesConfigurator getDevServicesConfigurator(List<KeycloakDevServicesRequiredBuildItem> items) {
return new KeycloakDevServicesConfigurator() {
@Override
public Map<String, String> createProperties(ConfigPropertiesContext context) {
return items
.stream()
.map(i -> i.devServicesConfigurator)
.map(producer -> producer.createProperties(context))
.map(Map::entrySet)
.flatMap(Collection::stream)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

@Override
public void customizeDefaultRealm(RealmRepresentation realmRepresentation) {
items
.stream()
.map(i -> i.devServicesConfigurator)
.forEach(i -> i.customizeDefaultRealm(realmRepresentation));
}
};
}

public static KeycloakDevServicesRequiredBuildItem requireDevServiceForOidcClient() {
return new KeycloakDevServicesRequiredBuildItem(Capability.OIDC_CLIENT);
}
}
8 changes: 4 additions & 4 deletions extensions/oidc-client-registration/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-common-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-devservices-keycloak</artifactId>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
Expand Down Expand Up @@ -83,10 +87,6 @@
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>false</skip>
<systemPropertyVariables>
<keycloak.docker.image>${keycloak.docker.legacy.image}</keycloak.docker.image>
<keycloak.use.https>false</keycloak.use.https>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package io.quarkus.oidc.client.registration.deployment.devservices;

import java.util.List;
import java.util.Map;

import org.eclipse.microprofile.config.ConfigProvider;
import org.jboss.logging.Logger;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.representations.idm.ComponentExportRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;

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;
import io.quarkus.deployment.builditem.DockerStatusBuildItem;
import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig;
import io.quarkus.devservices.keycloak.KeycloakAdminPageBuildItem;
import io.quarkus.devservices.keycloak.KeycloakDevServicesConfigurator;
import io.quarkus.devservices.keycloak.KeycloakDevServicesRequiredBuildItem;
import io.quarkus.devui.spi.page.CardPageBuildItem;
import io.quarkus.oidc.client.registration.deployment.OidcClientRegistrationBuildStep;
import io.quarkus.runtime.configuration.ConfigUtils;

@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = { OidcClientRegistrationBuildStep.IsEnabled.class,
GlobalDevServicesConfig.Enabled.class })
public class OidcClientRegistrationDevServicesBuildStep {

private static final Logger LOG = Logger.getLogger(OidcClientRegistrationDevServicesBuildStep.class);
private static final String OIDC_CLIENT_REG_AUTH_SERVER_URL_CONFIG_KEY = "quarkus.oidc-client-registration.auth-server-url";
private static final String OIDC_AUTH_SERVER_URL_CONFIG_KEY = "quarkus.oidc.auth-server-url";

@BuildStep
void requireKeycloakDevService(BuildProducer<KeycloakDevServicesRequiredBuildItem> keycloakDevSvcRequiredProducer,
DockerStatusBuildItem dockerStatusBuildItem) {
if (ConfigUtils.isPropertyNonEmpty(OIDC_CLIENT_REG_AUTH_SERVER_URL_CONFIG_KEY)) {
LOG.debugf("Not starting Dev Services for Keycloak as '%s' has been provided",
OIDC_CLIENT_REG_AUTH_SERVER_URL_CONFIG_KEY);
return;
}
// this is for backwards compatibility - when users configured named client with OIDC auth server URL
// that is not our Keycloak Dev Service, we don't want to start our Dev Service to keep previous behavior
if (ConfigUtils.isPropertyNonEmpty(OIDC_AUTH_SERVER_URL_CONFIG_KEY)) {
LOG.debugf("Not starting Dev Services for Keycloak as '%s' has been provided",
OIDC_AUTH_SERVER_URL_CONFIG_KEY);
return;
}
if (!dockerStatusBuildItem.isContainerRuntimeAvailable()) {
LOG.warnf("Please configure '%s' or get a working docker instance", OIDC_CLIENT_REG_AUTH_SERVER_URL_CONFIG_KEY);
return;
}

var devServicesConfigurator = new KeycloakDevServicesConfigurator() {

@Override
public Map<String, String> createProperties(ConfigPropertiesContext ctx) {
return Map.of(OIDC_CLIENT_REG_AUTH_SERVER_URL_CONFIG_KEY, ctx.authServerInternalUrl());
}

@Override
public void customizeDefaultRealm(RealmRepresentation realmRepresentation) {
if (getInitialToken() == null) {
realmRepresentation.setRegistrationAllowed(true);
realmRepresentation.setRegistrationFlow("registration");
if (realmRepresentation.getComponents() == null) {
realmRepresentation.setComponents(new MultivaluedHashMap<>());
}
var componentExportRepresentation = new ComponentExportRepresentation();
componentExportRepresentation.setName("Full Scope Disabled");
componentExportRepresentation.setProviderId("scope");
componentExportRepresentation.setSubType("anonymous");
realmRepresentation.getComponents().put(
"org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy",
List.of(componentExportRepresentation));
}
}
};

keycloakDevSvcRequiredProducer
.produce(new KeycloakDevServicesRequiredBuildItem(devServicesConfigurator));
}

@BuildStep(onlyIf = IsDevelopment.class)
KeycloakAdminPageBuildItem addCardWithLinkToKeycloakAdmin() {
return new KeycloakAdminPageBuildItem(new CardPageBuildItem());
}

private static String getInitialToken() {
return ConfigProvider.getConfig().getOptionalValue("quarkus.oidc-client-registration.initial-token", String.class)
.orElse(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.quarkus.oidc.client.registration;

import static org.junit.jupiter.api.Assertions.assertEquals;

import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.runtime.StartupEvent;
import io.quarkus.test.QuarkusUnitTest;

public class OidcClientRegistrationKeycloakDevServiceTest {

@RegisterExtension
static final QuarkusUnitTest test = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addAsResource(
new StringAsset(
"""
quarkus.oidc-client-registration.metadata.client-name=Default Test Client
quarkus.oidc-client-registration.metadata.redirect-uri=http://localhost:8081/default/redirect
quarkus.oidc-client-registration.named.metadata.client-name=Named Test Client
quarkus.oidc-client-registration.named.metadata.redirect-uri=http://localhost:8081/named/redirect
quarkus.oidc-client-registration.named.auth-server-url=${quarkus.oidc-client-registration.auth-server-url}
"""),
"application.properties"));

@Inject
TestClientRegistrations testClientRegistrations;

@Test
public void testDefaultRegisteredClient() {
assertEquals("Default Test Client", testClientRegistrations.defaultClientMetadata.getClientName());
assertEquals("http://localhost:8081/default/redirect",
testClientRegistrations.defaultClientMetadata.getRedirectUris().get(0));
}

@Test
public void testNamedRegisteredClient() {
assertEquals("Named Test Client", testClientRegistrations.namedClientMetadata.getClientName());
assertEquals("http://localhost:8081/named/redirect",
testClientRegistrations.namedClientMetadata.getRedirectUris().get(0));
}

@Singleton
public static final class TestClientRegistrations {

private volatile ClientMetadata defaultClientMetadata;
private volatile ClientMetadata namedClientMetadata;

void prepareDefaultClientMetadata(@Observes StartupEvent event, OidcClientRegistrations clientRegistrations) {
var clientRegistration = clientRegistrations.getClientRegistration();
var registeredClient = clientRegistration.registeredClient().await().indefinitely();
defaultClientMetadata = registeredClient.metadata();

clientRegistration = clientRegistrations.getClientRegistration("named");
registeredClient = clientRegistration.registeredClient().await().indefinitely();
namedClientMetadata = registeredClient.metadata();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,11 @@ public static Uni<OidcClientRegistration> createOidcClientRegistrationUni(OidcCl
if (isEmptyMetadata(oidcConfig.metadata)) {
return Uni.createFrom().nullItem();
}
var clientName = DEFAULT_ID.equals(oidcConfig.id.orElse(DEFAULT_ID)) ? "" : "." + oidcConfig.id.get();
throw new ConfigurationException(
"Either 'quarkus.oidc-client-registration.auth-server-url' or absolute 'quarkus.oidc-client-registration.registration-path' URL must be set");
"Either 'quarkus.oidc-client-registration" + clientName
+ ".auth-server-url' or absolute 'quarkus.oidc-client-registration" + clientName
+ ".registration-path' URL must be set");
}
OidcCommonUtils.verifyEndpointUrl(getEndpointUrl(oidcConfig));
} catch (Throwable t) {
Expand Down Expand Up @@ -194,7 +197,7 @@ public Uni<OidcClientRegistration> apply(OidcConfigurationMetadata metadata, Thr
public OidcClientRegistration apply(RegisteredClient r, Throwable t2) {
RegisteredClient registeredClient;
if (t2 != null) {
LOG.errorf("%s client registartion failed: %s, it can be retried later",
LOG.errorf("%s client registration failed: %s, it can be retried later",
oidcConfig.id.orElse(DEFAULT_ID), t2.getMessage());
registeredClient = null;
} else {
Expand Down
Loading

0 comments on commit 19c8c62

Please sign in to comment.