Skip to content

Commit 91cc2dd

Browse files
authored
Merge pull request #44270 from michalvavrik/feature/keycloak-dev-svc-for-admin-and-registration
Support Keycloak Dev Services for standalone OIDC Client Registration extension
2 parents 92e09b9 + 6a34154 commit 91cc2dd

File tree

14 files changed

+462
-209
lines changed

14 files changed

+462
-209
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.quarkus.devservices.keycloak;
2+
3+
import io.quarkus.builder.item.MultiBuildItem;
4+
import io.quarkus.devui.spi.page.CardPageBuildItem;
5+
6+
/**
7+
* Extensions should produce this build item if a DEV UI card with
8+
* the Keycloak Admin link should be created for the extension.
9+
*/
10+
public final class KeycloakAdminPageBuildItem extends MultiBuildItem {
11+
12+
final CardPageBuildItem cardPage;
13+
14+
/**
15+
* @param cardPage created inside extension that requires Keycloak Dev Service, this way, card page
16+
* custom identifier deduced from a stacktrace walker will identify the extension correctly
17+
*/
18+
public KeycloakAdminPageBuildItem(CardPageBuildItem cardPage) {
19+
this.cardPage = cardPage;
20+
}
21+
}

extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesConfigBuildItem.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package io.quarkus.devservices.keycloak;
22

3+
import static io.quarkus.devservices.keycloak.KeycloakDevServicesProcessor.KEYCLOAK_URL_KEY;
4+
35
import java.util.Map;
6+
import java.util.Optional;
47

58
import io.quarkus.builder.item.SimpleBuildItem;
69

@@ -28,4 +31,11 @@ public Map<String, String> getConfig() {
2831
public boolean isContainerRestarted() {
2932
return containerRestarted;
3033
}
34+
35+
public static String getKeycloakUrl(Optional<KeycloakDevServicesConfigBuildItem> configBuildItem) {
36+
return configBuildItem
37+
.map(KeycloakDevServicesConfigBuildItem::getConfig)
38+
.map(config -> config.get(KEYCLOAK_URL_KEY))
39+
.orElse(null);
40+
}
3141
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.quarkus.devservices.keycloak;
2+
3+
import java.util.Map;
4+
5+
import org.keycloak.representations.idm.RealmRepresentation;
6+
7+
public interface KeycloakDevServicesConfigurator {
8+
9+
record ConfigPropertiesContext(String authServerInternalUrl, String oidcClientId, String oidcClientSecret) {
10+
}
11+
12+
Map<String, String> createProperties(ConfigPropertiesContext context);
13+
14+
default void customizeDefaultRealm(RealmRepresentation realmRepresentation) {
15+
}
16+
17+
}

extensions/devservices/keycloak/src/main/java/io/quarkus/devservices/keycloak/KeycloakDevServicesProcessor.java

Lines changed: 103 additions & 134 deletions
Large diffs are not rendered by default.
Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
package io.quarkus.devservices.keycloak;
22

3-
import static io.quarkus.devservices.keycloak.KeycloakDevServicesProcessor.OIDC_CLIENT_AUTH_SERVER_URL_CONFIG_KEY;
4-
import static io.quarkus.devservices.keycloak.KeycloakDevServicesProcessor.OIDC_CLIENT_TOKEN_PATH_CONFIG_KEY;
3+
import static java.util.Objects.requireNonNull;
54

5+
import java.util.Arrays;
6+
import java.util.Collection;
67
import java.util.List;
8+
import java.util.Map;
9+
import java.util.stream.Collectors;
10+
import java.util.stream.Stream;
11+
12+
import org.jboss.logging.Logger;
13+
import org.keycloak.representations.idm.RealmRepresentation;
714

815
import io.quarkus.builder.item.MultiBuildItem;
916
import io.quarkus.runtime.configuration.ConfigUtils;
@@ -15,33 +22,65 @@
1522
*/
1623
public final class KeycloakDevServicesRequiredBuildItem extends MultiBuildItem {
1724

18-
enum Capability {
19-
OIDC,
20-
OIDC_CLIENT
21-
}
25+
private static final Logger LOG = Logger.getLogger(KeycloakDevServicesProcessor.class);
26+
public static final String OIDC_AUTH_SERVER_URL_CONFIG_KEY = "quarkus.oidc.auth-server-url";
2227

23-
private final Capability capability;
28+
private final KeycloakDevServicesConfigurator devServicesConfigurator;
29+
private final String authServerUrl;
2430

25-
private KeycloakDevServicesRequiredBuildItem(Capability capability) {
26-
this.capability = capability;
31+
private KeycloakDevServicesRequiredBuildItem(KeycloakDevServicesConfigurator devServicesConfigurator,
32+
String authServerUrl) {
33+
this.devServicesConfigurator = requireNonNull(devServicesConfigurator);
34+
this.authServerUrl = requireNonNull(authServerUrl);
2735
}
2836

29-
static boolean setOidcConfigProperties(List<KeycloakDevServicesRequiredBuildItem> items) {
30-
return items.stream().anyMatch(i -> i.capability == Capability.OIDC);
37+
String getAuthServerUrl() {
38+
return authServerUrl;
3139
}
3240

33-
static boolean setOidcClientConfigProperties(List<KeycloakDevServicesRequiredBuildItem> items) {
34-
boolean serverUrlOrTokenPathConfigured = ConfigUtils.isPropertyNonEmpty(OIDC_CLIENT_AUTH_SERVER_URL_CONFIG_KEY)
35-
|| ConfigUtils.isPropertyNonEmpty(OIDC_CLIENT_TOKEN_PATH_CONFIG_KEY);
36-
return !serverUrlOrTokenPathConfigured
37-
&& items.stream().anyMatch(i -> i.capability == Capability.OIDC_CLIENT);
41+
public static KeycloakDevServicesRequiredBuildItem of(KeycloakDevServicesConfigurator devServicesConfigurator,
42+
String authServerUrl, String... dontStartConfigProperties) {
43+
if (shouldStartDevService(dontStartConfigProperties, authServerUrl)) {
44+
return new KeycloakDevServicesRequiredBuildItem(devServicesConfigurator, authServerUrl);
45+
}
46+
return null;
47+
}
48+
49+
static KeycloakDevServicesConfigurator getDevServicesConfigurator(List<KeycloakDevServicesRequiredBuildItem> items) {
50+
return new KeycloakDevServicesConfigurator() {
51+
@Override
52+
public Map<String, String> createProperties(ConfigPropertiesContext context) {
53+
return items
54+
.stream()
55+
.map(i -> i.devServicesConfigurator)
56+
.map(producer -> producer.createProperties(context))
57+
.map(Map::entrySet)
58+
.flatMap(Collection::stream)
59+
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
60+
}
61+
62+
@Override
63+
public void customizeDefaultRealm(RealmRepresentation realmRepresentation) {
64+
items
65+
.stream()
66+
.map(i -> i.devServicesConfigurator)
67+
.forEach(i -> i.customizeDefaultRealm(realmRepresentation));
68+
}
69+
};
3870
}
3971

40-
public static KeycloakDevServicesRequiredBuildItem requireDevServiceForOidc() {
41-
return new KeycloakDevServicesRequiredBuildItem(Capability.OIDC);
72+
private static boolean shouldStartDevService(String[] dontStartConfigProperties, String authServerUrl) {
73+
return Stream
74+
.concat(Stream.of(authServerUrl), Arrays.stream(dontStartConfigProperties))
75+
.allMatch(KeycloakDevServicesRequiredBuildItem::shouldStartDevService);
4276
}
4377

44-
public static KeycloakDevServicesRequiredBuildItem requireDevServiceForOidcClient() {
45-
return new KeycloakDevServicesRequiredBuildItem(Capability.OIDC_CLIENT);
78+
private static boolean shouldStartDevService(String dontStartConfigProperty) {
79+
if (ConfigUtils.isPropertyNonEmpty(dontStartConfigProperty)) {
80+
// this build item does not require to start the Keycloak Dev Service as runtime property was set
81+
LOG.debugf("Not starting Dev Services for Keycloak as '%s' has been provided", dontStartConfigProperty);
82+
return false;
83+
}
84+
return true;
4685
}
4786
}

extensions/oidc-client-registration/deployment/pom.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
<groupId>io.quarkus</groupId>
3232
<artifactId>quarkus-oidc-common-deployment</artifactId>
3333
</dependency>
34+
<dependency>
35+
<groupId>io.quarkus</groupId>
36+
<artifactId>quarkus-devservices-keycloak</artifactId>
37+
</dependency>
3438
<!-- Test dependencies -->
3539
<dependency>
3640
<groupId>io.quarkus</groupId>
@@ -83,10 +87,6 @@
8387
<artifactId>maven-surefire-plugin</artifactId>
8488
<configuration>
8589
<skip>false</skip>
86-
<systemPropertyVariables>
87-
<keycloak.docker.image>${keycloak.docker.legacy.image}</keycloak.docker.image>
88-
<keycloak.use.https>false</keycloak.use.https>
89-
</systemPropertyVariables>
9090
</configuration>
9191
</plugin>
9292
</plugins>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package io.quarkus.oidc.client.registration.deployment.devservices.keycloak;
2+
3+
import static io.quarkus.devservices.keycloak.KeycloakDevServicesRequiredBuildItem.OIDC_AUTH_SERVER_URL_CONFIG_KEY;
4+
5+
import java.util.List;
6+
import java.util.Map;
7+
8+
import org.eclipse.microprofile.config.ConfigProvider;
9+
import org.keycloak.common.util.MultivaluedHashMap;
10+
import org.keycloak.representations.idm.ComponentExportRepresentation;
11+
import org.keycloak.representations.idm.RealmRepresentation;
12+
13+
import io.quarkus.deployment.IsDevelopment;
14+
import io.quarkus.deployment.IsNormal;
15+
import io.quarkus.deployment.annotations.BuildStep;
16+
import io.quarkus.deployment.annotations.BuildSteps;
17+
import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig;
18+
import io.quarkus.devservices.keycloak.KeycloakAdminPageBuildItem;
19+
import io.quarkus.devservices.keycloak.KeycloakDevServicesConfigurator;
20+
import io.quarkus.devservices.keycloak.KeycloakDevServicesRequiredBuildItem;
21+
import io.quarkus.devui.spi.page.CardPageBuildItem;
22+
import io.quarkus.oidc.client.registration.deployment.OidcClientRegistrationBuildStep;
23+
24+
@BuildSteps(onlyIfNot = IsNormal.class, onlyIf = { OidcClientRegistrationBuildStep.IsEnabled.class,
25+
GlobalDevServicesConfig.Enabled.class })
26+
public class KeycloakDevServiceRequiredBuildStep {
27+
28+
private static final String OIDC_CLIENT_REG_AUTH_SERVER_URL_CONFIG_KEY = "quarkus.oidc-client-registration.auth-server-url";
29+
30+
@BuildStep
31+
KeycloakDevServicesRequiredBuildItem requireKeycloakDevService() {
32+
var devServicesConfigurator = new KeycloakDevServicesConfigurator() {
33+
34+
@Override
35+
public Map<String, String> createProperties(ConfigPropertiesContext ctx) {
36+
return Map.of(OIDC_CLIENT_REG_AUTH_SERVER_URL_CONFIG_KEY, ctx.authServerInternalUrl());
37+
}
38+
39+
@Override
40+
public void customizeDefaultRealm(RealmRepresentation realmRepresentation) {
41+
if (getInitialToken() == null) {
42+
realmRepresentation.setRegistrationAllowed(true);
43+
realmRepresentation.setRegistrationFlow("registration");
44+
if (realmRepresentation.getComponents() == null) {
45+
realmRepresentation.setComponents(new MultivaluedHashMap<>());
46+
}
47+
var componentExportRepresentation = new ComponentExportRepresentation();
48+
componentExportRepresentation.setName("Full Scope Disabled");
49+
componentExportRepresentation.setProviderId("scope");
50+
componentExportRepresentation.setSubType("anonymous");
51+
realmRepresentation.getComponents().put(
52+
"org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy",
53+
List.of(componentExportRepresentation));
54+
}
55+
}
56+
};
57+
58+
return KeycloakDevServicesRequiredBuildItem.of(devServicesConfigurator,
59+
OIDC_CLIENT_REG_AUTH_SERVER_URL_CONFIG_KEY, OIDC_AUTH_SERVER_URL_CONFIG_KEY);
60+
}
61+
62+
@BuildStep(onlyIf = IsDevelopment.class)
63+
KeycloakAdminPageBuildItem addCardWithLinkToKeycloakAdmin() {
64+
return new KeycloakAdminPageBuildItem(new CardPageBuildItem());
65+
}
66+
67+
private static String getInitialToken() {
68+
return ConfigProvider.getConfig().getOptionalValue("quarkus.oidc-client-registration.initial-token", String.class)
69+
.orElse(null);
70+
}
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package io.quarkus.oidc.client.registration;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import jakarta.enterprise.event.Observes;
6+
import jakarta.inject.Inject;
7+
import jakarta.inject.Singleton;
8+
9+
import org.jboss.shrinkwrap.api.asset.StringAsset;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.extension.RegisterExtension;
12+
13+
import io.quarkus.runtime.StartupEvent;
14+
import io.quarkus.test.QuarkusUnitTest;
15+
16+
public class OidcClientRegistrationKeycloakDevServiceTest {
17+
18+
@RegisterExtension
19+
static final QuarkusUnitTest test = new QuarkusUnitTest()
20+
.withApplicationRoot((jar) -> jar
21+
.addAsResource(
22+
new StringAsset(
23+
"""
24+
quarkus.oidc-client-registration.metadata.client-name=Default Test Client
25+
quarkus.oidc-client-registration.metadata.redirect-uri=http://localhost:8081/default/redirect
26+
quarkus.oidc-client-registration.named.metadata.client-name=Named Test Client
27+
quarkus.oidc-client-registration.named.metadata.redirect-uri=http://localhost:8081/named/redirect
28+
quarkus.oidc-client-registration.named.auth-server-url=${quarkus.oidc-client-registration.auth-server-url}
29+
"""),
30+
"application.properties"));
31+
32+
@Inject
33+
TestClientRegistrations testClientRegistrations;
34+
35+
@Test
36+
public void testDefaultRegisteredClient() {
37+
assertEquals("Default Test Client", testClientRegistrations.defaultClientMetadata.getClientName());
38+
assertEquals("http://localhost:8081/default/redirect",
39+
testClientRegistrations.defaultClientMetadata.getRedirectUris().get(0));
40+
}
41+
42+
@Test
43+
public void testNamedRegisteredClient() {
44+
assertEquals("Named Test Client", testClientRegistrations.namedClientMetadata.getClientName());
45+
assertEquals("http://localhost:8081/named/redirect",
46+
testClientRegistrations.namedClientMetadata.getRedirectUris().get(0));
47+
}
48+
49+
@Singleton
50+
public static final class TestClientRegistrations {
51+
52+
private volatile ClientMetadata defaultClientMetadata;
53+
private volatile ClientMetadata namedClientMetadata;
54+
55+
void prepareDefaultClientMetadata(@Observes StartupEvent event, OidcClientRegistrations clientRegistrations) {
56+
var clientRegistration = clientRegistrations.getClientRegistration();
57+
var registeredClient = clientRegistration.registeredClient().await().indefinitely();
58+
defaultClientMetadata = registeredClient.metadata();
59+
60+
clientRegistration = clientRegistrations.getClientRegistration("named");
61+
registeredClient = clientRegistration.registeredClient().await().indefinitely();
62+
namedClientMetadata = registeredClient.metadata();
63+
}
64+
}
65+
}

extensions/oidc-client-registration/runtime/src/main/java/io/quarkus/oidc/client/registration/runtime/OidcClientRegistrationRecorder.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,11 @@ public static Uni<OidcClientRegistration> createOidcClientRegistrationUni(OidcCl
105105
if (isEmptyMetadata(oidcConfig.metadata)) {
106106
return Uni.createFrom().nullItem();
107107
}
108+
var clientName = DEFAULT_ID.equals(oidcConfig.id.orElse(DEFAULT_ID)) ? "" : "." + oidcConfig.id.get();
108109
throw new ConfigurationException(
109-
"Either 'quarkus.oidc-client-registration.auth-server-url' or absolute 'quarkus.oidc-client-registration.registration-path' URL must be set");
110+
"Either 'quarkus.oidc-client-registration" + clientName
111+
+ ".auth-server-url' or absolute 'quarkus.oidc-client-registration" + clientName
112+
+ ".registration-path' URL must be set");
110113
}
111114
OidcCommonUtils.verifyEndpointUrl(getEndpointUrl(oidcConfig));
112115
} catch (Throwable t) {
@@ -194,7 +197,7 @@ public Uni<OidcClientRegistration> apply(OidcConfigurationMetadata metadata, Thr
194197
public OidcClientRegistration apply(RegisteredClient r, Throwable t2) {
195198
RegisteredClient registeredClient;
196199
if (t2 != null) {
197-
LOG.errorf("%s client registartion failed: %s, it can be retried later",
200+
LOG.errorf("%s client registration failed: %s, it can be retried later",
198201
oidcConfig.id.orElse(DEFAULT_ID), t2.getMessage());
199202
registeredClient = null;
200203
} else {

extensions/oidc-client/deployment/src/main/java/io/quarkus/oidc/client/deployment/OidcClientBuildStep.java

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@
2525
import io.quarkus.arc.processor.DotNames;
2626
import io.quarkus.deployment.ApplicationArchive;
2727
import io.quarkus.deployment.Feature;
28-
import io.quarkus.deployment.IsDevelopment;
29-
import io.quarkus.deployment.IsNormal;
3028
import io.quarkus.deployment.annotations.BuildProducer;
3129
import io.quarkus.deployment.annotations.BuildStep;
3230
import io.quarkus.deployment.annotations.BuildSteps;
@@ -37,11 +35,6 @@
3735
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
3836
import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem;
3937
import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem;
40-
import io.quarkus.deployment.dev.devservices.GlobalDevServicesConfig;
41-
import io.quarkus.devservices.keycloak.KeycloakDevServicesConfigBuildItem;
42-
import io.quarkus.devservices.keycloak.KeycloakDevServicesRequiredBuildItem;
43-
import io.quarkus.devui.spi.page.CardPageBuildItem;
44-
import io.quarkus.devui.spi.page.Page;
4538
import io.quarkus.gizmo.ClassCreator;
4639
import io.quarkus.gizmo.ClassOutput;
4740
import io.quarkus.gizmo.MethodCreator;
@@ -191,27 +184,6 @@ private AccessTokenInstanceBuildItem build() {
191184
return index.getIndex().getAnnotations(ACCESS_TOKEN).stream().map(ItemBuilder::new).map(ItemBuilder::build).toList();
192185
}
193186

194-
@BuildStep(onlyIfNot = IsNormal.class, onlyIf = GlobalDevServicesConfig.Enabled.class)
195-
KeycloakDevServicesRequiredBuildItem requireKeycloakDevService() {
196-
// this needs to be done as the shared Keycloak Dev Service doesn't know if the OIDC Client is enabled
197-
return KeycloakDevServicesRequiredBuildItem.requireDevServiceForOidcClient();
198-
}
199-
200-
@BuildStep(onlyIf = IsDevelopment.class)
201-
void produceDevUiCardWithKeycloakUrl(Optional<KeycloakDevServicesConfigBuildItem> configProps,
202-
BuildProducer<CardPageBuildItem> cardPageProducer) {
203-
final String keycloakAdminUrl = configProps.map(item -> item.getConfig().get("keycloak.url")).orElse(null);
204-
if (keycloakAdminUrl != null) {
205-
// Add Admin page
206-
final CardPageBuildItem cardPage = new CardPageBuildItem();
207-
cardPage.addPage(Page.externalPageBuilder("Keycloak Admin")
208-
.icon("font-awesome-solid:key")
209-
.doNotEmbed(true)
210-
.url(keycloakAdminUrl));
211-
cardPageProducer.produce(cardPage);
212-
}
213-
}
214-
215187
/**
216188
* Creates a Tokens producer class like follows:
217189
*

0 commit comments

Comments
 (0)