Skip to content

Commit

Permalink
Integrate Keycloak Admin Client with TLS registry
Browse files Browse the repository at this point in the history
  • Loading branch information
michalvavrik committed Sep 15, 2024
1 parent dac2bb6 commit c973daf
Show file tree
Hide file tree
Showing 14 changed files with 389 additions and 17 deletions.
14 changes: 14 additions & 0 deletions docs/src/main/asciidoc/security-keycloak-admin-client.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,20 @@ quarkus.keycloak.admin-client.grant-type=CLIENT_CREDENTIALS <1>

NOTE: Note that the xref:security-openid-connect-client.adoc[OidcClient] can also be used to acquire tokens.

== Configuring TLS

Check warning on line 198 in docs/src/main/asciidoc/security-keycloak-admin-client.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in 'Configuring TLS'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in 'Configuring TLS'.", "location": {"path": "docs/src/main/asciidoc/security-keycloak-admin-client.adoc", "range": {"start": {"line": 198, "column": 4}}}, "severity": "INFO"}

To configure a TLS connection for the Keycloak Admin Client, use the TLS Registry extension and point the Keycloak Admin Client to respective TLS configuration.
For example, you can configure mutual TLS (mTLS) like this:

[source,properties]
----
quarkus.keycloak.admin-client.tls-configuration-name=kc-mtls
quarkus.tls.kc-mtls.key-store.p12.path=client-keystore.p12
quarkus.tls.kc-mtls.key-store.p12.password=secret
quarkus.tls.kc-mtls.trust-store.p12.path=client-truststore.p12
quarkus.tls.kc-mtls.trust-store.p12.password=secret
----

== Testing

The preferred approach for testing Keycloak Admin Client against Keycloak is xref:security-openid-connect-dev-services.adoc[Dev Services for Keycloak].
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,11 @@ public Optional<String> scope() {
public GrantType grantType() {
return grantType;
}

@Override
public Optional<String> tlsConfigurationName() {
return Optional.empty();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,14 @@ public String asString() {
}
}

/**
* The name of the TLS configuration to use.
* <p>
* If a name is configured, it uses the configuration from {@code quarkus.tls.<name>.*}
* If a name is configured, but no TLS configuration is found with that name then an error will be thrown.
* <p>
* The default TLS configuration is <strong>not</strong> used by default.
*/
Optional<String> tlsConfigurationName();

}
5 changes: 5 additions & 0 deletions extensions/keycloak-admin-rest-client/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@
<artifactId>quarkus-oidc-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.smallrye.certs</groupId>
<artifactId>smallrye-certificate-generator-junit5</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io.quarkus.keycloak.admin.client.common.KeycloakAdminClientInjectionEnabled;
import io.quarkus.keycloak.admin.client.reactive.runtime.ResteasyReactiveClientProvider;
import io.quarkus.keycloak.admin.client.reactive.runtime.ResteasyReactiveKeycloakAdminClientRecorder;
import io.quarkus.tls.TlsRegistryBuildItem;

public class KeycloakAdminClientReactiveProcessor {

Expand Down Expand Up @@ -52,10 +53,16 @@ public void nativeImage(BuildProducer<ServiceProviderBuildItem> serviceProviderP
}

@Record(ExecutionTime.STATIC_INIT)
@BuildStep
void avoidRuntimeInitIssueInClientBuilderWrapper(ResteasyReactiveKeycloakAdminClientRecorder recorder) {
recorder.avoidRuntimeInitIssueInClientBuilderWrapper();
}

@Record(ExecutionTime.RUNTIME_INIT)
@Produce(ServiceStartBuildItem.class)
@BuildStep
public void integrate(ResteasyReactiveKeycloakAdminClientRecorder recorder) {
recorder.setClientProvider();
public void integrate(ResteasyReactiveKeycloakAdminClientRecorder recorder, TlsRegistryBuildItem tlsRegistryBuildItem) {
recorder.setClientProvider(tlsRegistryBuildItem.registry());
}

@Record(ExecutionTime.RUNTIME_INIT)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package io.quarkus.keycloak.admin.client.reactive;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;

import org.hamcrest.Matchers;
import org.jboss.resteasy.reactive.RestPath;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.RolesRepresentation;

import io.quarkus.test.QuarkusDevModeTest;
import io.restassured.RestAssured;
import io.smallrye.certs.Format;
import io.smallrye.certs.junit5.Certificate;
import io.smallrye.certs.junit5.Certificates;

@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "mtls-test", password = "secret", formats = {
Format.PKCS12, Format.PEM }, client = true))
public class KeycloakAdminClientMutualTlsDevServicesTest {

@RegisterExtension
final static QuarkusDevModeTest app = new QuarkusDevModeTest()
.withApplicationRoot(jar -> jar
.addClasses(MtlsResource.class)
.addAsResource(new File("target/certs/mtls-test-keystore.p12"), "server-keystore.p12")
.addAsResource(new File("target/certs/mtls-test-server-ca.crt"), "server-ca.crt")
.addAsResource(new File("target/certs/mtls-test-client-keystore.p12"), "client-keystore.p12")
.addAsResource(new File("target/certs/mtls-test-client-truststore.p12"), "client-truststore.p12")
.addAsResource("app-mtls-config.properties", "application.properties"));

@Test
public void testCreateRealm() {
// create realm
RestAssured.given().post("/api/mtls").then().statusCode(204);
// test realm created
RestAssured.given().get("/api/mtls/Ron").then().statusCode(200).body(Matchers.is("Weasley"));
}

@Path("/api/mtls")
public static class MtlsResource {

@Inject
Keycloak keycloak;

@POST
public void createMtlsRealm() {
var realm = new RealmRepresentation();
realm.setRealm("mtls");
realm.setEnabled(true);
RolesRepresentation roles = new RolesRepresentation();
List<RoleRepresentation> realmRoles = new ArrayList<>();
roles.setRealm(realmRoles);
realm.setRoles(roles);
realm.getRoles().getRealm().add(new RoleRepresentation("Ron", "Weasley", false));
keycloak.realms().create(realm);
}

@Path("{roleName}")
@GET
public String getRoleDescription(@RestPath String roleName) {
return keycloak.realm("mtls").roles().get(roleName).toRepresentation().getDescription();
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Configure Dev Services for Keycloak
quarkus.keycloak.devservices.create-realm=false
quarkus.keycloak.devservices.port=8083
quarkus.keycloak.devservices.start-command=start --https-client-auth=required --hostname-strict=false --https-key-store-file=/etc/server-keystore.p12 --https-key-store-password=secret --truststore-paths=/etc/server-ca.crt --https-port=8080
# using PEM CA cert because generated PKCS12 server trust-store is encrypted, but KC requires no password for trust-store
quarkus.keycloak.devservices.resource-aliases.trust-store=server-ca.crt
quarkus.keycloak.devservices.resource-mappings.trust-store=/etc/server-ca.crt
quarkus.keycloak.devservices.resource-aliases.key-store=server-keystore.p12
quarkus.keycloak.devservices.resource-mappings.key-store=/etc/server-keystore.p12

# Configure Keycloak Admin Client
quarkus.keycloak.admin-client.server-url=https://localhost:${quarkus.keycloak.devservices.port}
quarkus.keycloak.admin-client.tls-configuration-name=kc-admin-client

# Mutual TLS configuration
quarkus.tls.kc-admin-client.key-store.p12.path=client-keystore.p12
quarkus.tls.kc-admin-client.key-store.p12.password=secret
quarkus.tls.kc-admin-client.trust-store.p12.path=client-truststore.p12
quarkus.tls.kc-admin-client.trust-store.p12.password=secret
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.quarkus.keycloak.admin.client.reactive.runtime;

import java.security.KeyStore;
import java.util.List;
import java.util.Optional;

import javax.net.ssl.SSLContext;

Expand All @@ -11,6 +13,7 @@
import jakarta.ws.rs.core.MediaType;

import org.eclipse.microprofile.config.ConfigProvider;
import org.jboss.resteasy.reactive.client.TlsConfig;
import org.jboss.resteasy.reactive.client.api.ClientLogger;
import org.jboss.resteasy.reactive.client.impl.ClientBuilderImpl;
import org.jboss.resteasy.reactive.client.impl.WebTargetImpl;
Expand All @@ -24,6 +27,10 @@
import io.quarkus.arc.InstanceHandle;
import io.quarkus.jackson.ObjectMapperCustomizer;
import io.quarkus.rest.client.reactive.jackson.runtime.serialisers.ClientJacksonMessageBodyWriter;
import io.quarkus.tls.TlsConfiguration;
import io.vertx.core.net.KeyCertOptions;
import io.vertx.core.net.SSLOptions;
import io.vertx.core.net.TrustOptions;

public class ResteasyReactiveClientProvider implements ResteasyClientProvider {

Expand All @@ -32,14 +39,26 @@ public class ResteasyReactiveClientProvider implements ResteasyClientProvider {
private static final int READER_PROVIDER_PRIORITY = Priorities.USER - 100; // ensures that it will be used first

private final boolean tlsTrustAll;
private final TlsConfig tlsConfig;

public ResteasyReactiveClientProvider(boolean tlsTrustAll) {
this.tlsTrustAll = tlsTrustAll;
this.tlsConfig = null;
}

public ResteasyReactiveClientProvider(TlsConfiguration tlsConfiguration) {
tlsTrustAll = tlsConfiguration.isTrustAll();
this.tlsConfig = createTlsConfig(tlsConfiguration);
}

@Override
public Client newRestEasyClient(Object messageHandler, SSLContext sslContext, boolean disableTrustManager) {
ClientBuilderImpl clientBuilder = new ClientBuilderImpl().trustAll(tlsTrustAll || disableTrustManager);
ClientBuilderImpl clientBuilder = new ClientBuilderImpl();
if (tlsConfig == null) {
clientBuilder.trustAll(tlsTrustAll || disableTrustManager);
} else {
clientBuilder.tlsConfig(tlsConfig);
}
return registerJacksonProviders(clientBuilder).build();
}

Expand Down Expand Up @@ -126,4 +145,53 @@ private ObjectMapper getObjectMapper(ObjectMapper value,
public <R> R targetProxy(WebTarget target, Class<R> targetClass) {
return ((WebTargetImpl) target).proxy(targetClass);
}

private static TlsConfig createTlsConfig(TlsConfiguration tlsConfiguration) {
return new TlsConfig() {
@Override
public KeyStore getKeyStore() {
return tlsConfiguration.getKeyStore();
}

@Override
public KeyCertOptions getKeyStoreOptions() {
return tlsConfiguration.getKeyStoreOptions();
}

@Override
public KeyStore getTrustStore() {
return tlsConfiguration.getTrustStore();
}

@Override
public TrustOptions getTrustStoreOptions() {
return tlsConfiguration.getTrustStoreOptions();
}

@Override
public SSLOptions getSSLOptions() {
return tlsConfiguration.getSSLOptions();
}

@Override
public SSLContext createSSLContext() throws Exception {
return tlsConfiguration.createSSLContext();
}

@Override
public Optional<String> getHostnameVerificationAlgorithm() {
return tlsConfiguration.getHostnameVerificationAlgorithm();
}

@Override
public boolean usesSni() {
return tlsConfiguration.usesSni();
}

@Override
public boolean isTrustAll() {
return tlsConfiguration.isTrustAll();
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@

import java.util.function.Supplier;

import org.eclipse.microprofile.config.ConfigProvider;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;

import io.quarkus.keycloak.admin.client.common.KeycloakAdminClientConfig;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.runtime.annotations.Recorder;
import io.quarkus.tls.TlsConfiguration;
import io.quarkus.tls.TlsConfigurationRegistry;

@Recorder
public class ResteasyReactiveKeycloakAdminClientRecorder {
Expand All @@ -22,9 +23,21 @@ public ResteasyReactiveKeycloakAdminClientRecorder(
this.keycloakAdminClientConfigRuntimeValue = keycloakAdminClientConfigRuntimeValue;
}

public void setClientProvider() {
boolean trustAll = ConfigProvider.getConfig().getOptionalValue("quarkus.tls.trust-all", Boolean.class).orElse(false);
Keycloak.setClientProvider(new ResteasyReactiveClientProvider(trustAll));
public void setClientProvider(Supplier<TlsConfigurationRegistry> registrySupplier) {
var registry = registrySupplier.get();
var namedTlsConfig = TlsConfiguration.from(registry,
keycloakAdminClientConfigRuntimeValue.getValue().tlsConfigurationName());
if (namedTlsConfig.isPresent()) {
Keycloak.setClientProvider(new ResteasyReactiveClientProvider(namedTlsConfig.get()));
} else {
final boolean trustAll;
if (registry.getDefault().isPresent()) {
trustAll = registry.getDefault().get().isTrustAll();
} else {
trustAll = false;
}
Keycloak.setClientProvider(new ResteasyReactiveClientProvider(trustAll));
}
}

public Supplier<Keycloak> createAdminClient() {
Expand Down Expand Up @@ -57,4 +70,11 @@ public Keycloak get() {
}
};
}

public void avoidRuntimeInitIssueInClientBuilderWrapper() {
// we set our provider at runtime, it is not used before that
// however org.keycloak.admin.client.Keycloak.CLIENT_PROVIDER is initialized during
// static init with org.keycloak.admin.client.ClientBuilderWrapper that is not compatible with native mode
Keycloak.setClientProvider(null);
}
}
5 changes: 5 additions & 0 deletions extensions/keycloak-admin-resteasy-client/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@
<artifactId>quarkus-oidc-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.smallrye.certs</groupId>
<artifactId>smallrye-certificate-generator-junit5</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyIgnoreWarningBuildItem;
import io.quarkus.keycloak.admin.client.common.KeycloakAdminClientInjectionEnabled;
import io.quarkus.keycloak.adminclient.ResteasyKeycloakAdminClientRecorder;
import io.quarkus.tls.TlsRegistryBuildItem;

public class KeycloakAdminClientProcessor {

Expand All @@ -49,12 +50,19 @@ ReflectiveClassBuildItem reflect() {
}

@Record(ExecutionTime.STATIC_INIT)
@BuildStep
void avoidRuntimeInitIssueInClientBuilderWrapper(ResteasyKeycloakAdminClientRecorder recorder) {
recorder.avoidRuntimeInitIssueInClientBuilderWrapper();
}

@Record(ExecutionTime.RUNTIME_INIT)
@Produce(ServiceStartBuildItem.class)
@BuildStep
public void integrate(ResteasyKeycloakAdminClientRecorder recorder, Capabilities capabilities) {
public void integrate(ResteasyKeycloakAdminClientRecorder recorder, Capabilities capabilities,
TlsRegistryBuildItem tlsRegistryBuildItem) {
boolean areJSONBProvidersPresent = capabilities.isPresent(Capability.RESTEASY_JSON_JSONB)
|| capabilities.isPresent(Capability.RESTEASY_JSON_JSONB_CLIENT);
recorder.setClientProvider(areJSONBProvidersPresent);
recorder.setClientProvider(areJSONBProvidersPresent, tlsRegistryBuildItem.registry());
}

@Record(ExecutionTime.RUNTIME_INIT)
Expand Down
Loading

0 comments on commit c973daf

Please sign in to comment.