diff --git a/docs/src/main/asciidoc/security-keycloak-admin-client.adoc b/docs/src/main/asciidoc/security-keycloak-admin-client.adoc index b4d70655ef1b4..64c960ccdb43b 100644 --- a/docs/src/main/asciidoc/security-keycloak-admin-client.adoc +++ b/docs/src/main/asciidoc/security-keycloak-admin-client.adoc @@ -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 + +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]. diff --git a/extensions/keycloak-admin-client-common/deployment/src/test/java/io/quarkus/keycloak/admin/client/common/ConfigValidationTest.java b/extensions/keycloak-admin-client-common/deployment/src/test/java/io/quarkus/keycloak/admin/client/common/ConfigValidationTest.java index b13085a943fd2..43cd6ad06e27e 100644 --- a/extensions/keycloak-admin-client-common/deployment/src/test/java/io/quarkus/keycloak/admin/client/common/ConfigValidationTest.java +++ b/extensions/keycloak-admin-client-common/deployment/src/test/java/io/quarkus/keycloak/admin/client/common/ConfigValidationTest.java @@ -114,6 +114,11 @@ public Optional scope() { public GrantType grantType() { return grantType; } + + @Override + public Optional tlsConfigurationName() { + return Optional.empty(); + } } } diff --git a/extensions/keycloak-admin-client-common/runtime/src/main/java/io/quarkus/keycloak/admin/client/common/KeycloakAdminClientConfig.java b/extensions/keycloak-admin-client-common/runtime/src/main/java/io/quarkus/keycloak/admin/client/common/KeycloakAdminClientConfig.java index df54a9515992c..004200936b2bd 100644 --- a/extensions/keycloak-admin-client-common/runtime/src/main/java/io/quarkus/keycloak/admin/client/common/KeycloakAdminClientConfig.java +++ b/extensions/keycloak-admin-client-common/runtime/src/main/java/io/quarkus/keycloak/admin/client/common/KeycloakAdminClientConfig.java @@ -71,4 +71,14 @@ public String asString() { } } + /** + * The name of the TLS configuration to use. + *

+ * If a name is configured, it uses the configuration from {@code quarkus.tls..*} + * If a name is configured, but no TLS configuration is found with that name then an error will be thrown. + *

+ * The default TLS configuration is not used by default. + */ + Optional tlsConfigurationName(); + } diff --git a/extensions/keycloak-admin-rest-client/deployment/pom.xml b/extensions/keycloak-admin-rest-client/deployment/pom.xml index a8fe4071dfff9..8ff302c1ac65f 100644 --- a/extensions/keycloak-admin-rest-client/deployment/pom.xml +++ b/extensions/keycloak-admin-rest-client/deployment/pom.xml @@ -50,6 +50,11 @@ quarkus-oidc-deployment test + + io.smallrye.certs + smallrye-certificate-generator-junit5 + test + diff --git a/extensions/keycloak-admin-rest-client/deployment/src/main/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientReactiveProcessor.java b/extensions/keycloak-admin-rest-client/deployment/src/main/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientReactiveProcessor.java index 72e85714da22c..d233e7aa78ceb 100644 --- a/extensions/keycloak-admin-rest-client/deployment/src/main/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientReactiveProcessor.java +++ b/extensions/keycloak-admin-rest-client/deployment/src/main/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientReactiveProcessor.java @@ -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 { @@ -52,10 +53,16 @@ public void nativeImage(BuildProducer 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) diff --git a/extensions/keycloak-admin-rest-client/deployment/src/test/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientMutualTlsDevServicesTest.java b/extensions/keycloak-admin-rest-client/deployment/src/test/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientMutualTlsDevServicesTest.java new file mode 100644 index 0000000000000..01542b0770234 --- /dev/null +++ b/extensions/keycloak-admin-rest-client/deployment/src/test/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientMutualTlsDevServicesTest.java @@ -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 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(); + } + + } +} diff --git a/extensions/keycloak-admin-rest-client/deployment/src/test/resources/app-mtls-config.properties b/extensions/keycloak-admin-rest-client/deployment/src/test/resources/app-mtls-config.properties new file mode 100644 index 0000000000000..5d86f89b4eca6 --- /dev/null +++ b/extensions/keycloak-admin-rest-client/deployment/src/test/resources/app-mtls-config.properties @@ -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 diff --git a/extensions/keycloak-admin-rest-client/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java b/extensions/keycloak-admin-rest-client/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java index a8004dd19c049..ad35c9f994dba 100644 --- a/extensions/keycloak-admin-rest-client/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java +++ b/extensions/keycloak-admin-rest-client/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java @@ -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; @@ -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; @@ -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 { @@ -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(); } @@ -126,4 +145,53 @@ private ObjectMapper getObjectMapper(ObjectMapper value, public R targetProxy(WebTarget target, Class 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 getHostnameVerificationAlgorithm() { + return tlsConfiguration.getHostnameVerificationAlgorithm(); + } + + @Override + public boolean usesSni() { + return tlsConfiguration.usesSni(); + } + + @Override + public boolean isTrustAll() { + return tlsConfiguration.isTrustAll(); + } + }; + } } diff --git a/extensions/keycloak-admin-rest-client/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java b/extensions/keycloak-admin-rest-client/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java index a1750ed2db12b..0d99d1ec027f0 100644 --- a/extensions/keycloak-admin-rest-client/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java +++ b/extensions/keycloak-admin-rest-client/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java @@ -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 { @@ -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 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 createAdminClient() { @@ -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); + } } diff --git a/extensions/keycloak-admin-resteasy-client/deployment/pom.xml b/extensions/keycloak-admin-resteasy-client/deployment/pom.xml index 9c07f98455388..882b34d8c8230 100644 --- a/extensions/keycloak-admin-resteasy-client/deployment/pom.xml +++ b/extensions/keycloak-admin-resteasy-client/deployment/pom.xml @@ -58,6 +58,11 @@ quarkus-oidc-deployment test + + io.smallrye.certs + smallrye-certificate-generator-junit5 + test + diff --git a/extensions/keycloak-admin-resteasy-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java b/extensions/keycloak-admin-resteasy-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java index f39602ec018a1..1a5726456b56c 100644 --- a/extensions/keycloak-admin-resteasy-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java +++ b/extensions/keycloak-admin-resteasy-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java @@ -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 { @@ -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) diff --git a/extensions/keycloak-admin-resteasy-client/deployment/src/test/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientMutualTlsDevServicesTest.java b/extensions/keycloak-admin-resteasy-client/deployment/src/test/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientMutualTlsDevServicesTest.java new file mode 100644 index 0000000000000..b628adb65aed5 --- /dev/null +++ b/extensions/keycloak-admin-resteasy-client/deployment/src/test/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientMutualTlsDevServicesTest.java @@ -0,0 +1,75 @@ +package io.quarkus.keycloak.adminclient.deployment; + +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 jakarta.ws.rs.PathParam; + +import org.hamcrest.Matchers; +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 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(@PathParam("roleName") String roleName) { + return keycloak.realm("mtls").roles().get(roleName).toRepresentation().getDescription(); + } + + } +} diff --git a/extensions/keycloak-admin-resteasy-client/deployment/src/test/resources/app-mtls-config.properties b/extensions/keycloak-admin-resteasy-client/deployment/src/test/resources/app-mtls-config.properties new file mode 100644 index 0000000000000..5d86f89b4eca6 --- /dev/null +++ b/extensions/keycloak-admin-resteasy-client/deployment/src/test/resources/app-mtls-config.properties @@ -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 diff --git a/extensions/keycloak-admin-resteasy-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java b/extensions/keycloak-admin-resteasy-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java index b16cf1dff0de0..069f72648e1b3 100644 --- a/extensions/keycloak-admin-resteasy-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java +++ b/extensions/keycloak-admin-resteasy-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java @@ -9,18 +9,21 @@ import jakarta.ws.rs.Consumes; import jakarta.ws.rs.Produces; import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.WebTarget; import jakarta.ws.rs.core.MediaType; -import org.eclipse.microprofile.config.ConfigProvider; -import org.keycloak.admin.client.ClientBuilderWrapper; +import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget; +import org.jboss.resteasy.client.jaxrs.internal.ResteasyClientBuilderImpl; import org.keycloak.admin.client.Keycloak; import org.keycloak.admin.client.KeycloakBuilder; -import org.keycloak.admin.client.spi.ResteasyClientClassicProvider; +import org.keycloak.admin.client.spi.ResteasyClientProvider; import io.quarkus.keycloak.admin.client.common.KeycloakAdminClientConfig; import io.quarkus.resteasy.common.runtime.jackson.QuarkusJacksonSerializer; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.tls.TlsConfiguration; +import io.quarkus.tls.TlsConfigurationRegistry; @Recorder public class ResteasyKeycloakAdminClientRecorder { @@ -63,24 +66,63 @@ public Keycloak get() { }; } - public void setClientProvider(boolean areJSONBProvidersPresent) { - Keycloak.setClientProvider(new ResteasyClientClassicProvider() { + public void setClientProvider(boolean areJSONBProvidersPresent, Supplier registrySupplier) { + var registry = registrySupplier.get(); + var namedTlsConfig = TlsConfiguration.from(registry, + keycloakAdminClientConfigRuntimeValue.getValue().tlsConfigurationName()).orElse(null); + final boolean globalTrustAll; + if (registry.getDefault().isPresent()) { + globalTrustAll = registry.getDefault().get().isTrustAll(); + } else { + globalTrustAll = false; + } + + Keycloak.setClientProvider(new ResteasyClientProvider() { @Override public Client newRestEasyClient(Object customJacksonProvider, SSLContext sslContext, boolean disableTrustManager) { - boolean trustAll = ConfigProvider.getConfig().getOptionalValue("quarkus.tls.trust-all", Boolean.class) - .orElse(false); + // this is what 'org.keycloak.admin.client.ClientBuilderWrapper.create' does + var builder = new ResteasyClientBuilderImpl(); + builder.connectionPoolSize(10); + + if (namedTlsConfig == null) { + builder.sslContext(sslContext); + if (globalTrustAll) { + builder.disableTrustManager(); + } + } else { + if (namedTlsConfig.isTrustAll()) { + builder.disableTrustManager(); + } + try { + builder.sslContext(namedTlsConfig.createSSLContext()); + } catch (Exception e) { + throw new RuntimeException("Failed to create Keycloak Admin client SSLContext", e); + } + } + // point here is to use default Quarkus providers rather than org.keycloak.admin.client.JacksonProvider // as it doesn't work properly in native mode - var builder = ClientBuilderWrapper.create(sslContext, trustAll || disableTrustManager); if (areJSONBProvidersPresent) { // when both Jackson and JSONB providers are present, we need to ensure Jackson is used builder.register(new AppJsonQuarkusJacksonSerializer(), 100); } return builder.build(); } + + @Override + public R targetProxy(WebTarget webTarget, Class aClass) { + return (ResteasyWebTarget.class.cast(webTarget)).proxy(aClass); + } }); } + 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); + } + // makes media type more specific which ensures that it will be used first @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)