[Test Framework] Ability to run Keycloak test server with HTTPS (#42616)

* Ability to run Keycloak test server with HTTPS

Closes: #34486

Signed-off-by: Simon Vacek <simonvacky@email.cz>

# Conflicts:
#	test-framework/core/src/main/java/org/keycloak/testframework/CoreTestFrameworkExtension.java
#	test-framework/core/src/main/java/org/keycloak/testframework/server/KeycloakServerConfigBuilder.java

# Conflicts:
#	test-framework/core/src/main/java/org/keycloak/testframework/CoreTestFrameworkExtension.java

* PR review fixes

Signed-off-by: Simon Vacek <simonvacky@email.cz>

* Split keystore into truststore and keystore

Signed-off-by: Simon Vacek <simonvacky@email.cz>

---------

Signed-off-by: Simon Vacek <simonvacky@email.cz>
This commit is contained in:
Šimon Vacek 2025-10-06 12:56:51 +02:00 committed by GitHub
parent 54e8c87860
commit ae7c2d29e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 406 additions and 11 deletions

View File

@ -154,6 +154,11 @@ public class ClusteredKeycloakServer implements KeycloakServer {
return getManagementBaseUrl(0);
}
@Override
public boolean isTlsEnabled() {
return false;
}
public int getBasePort(int index) {
return containers[index].getMappedPort(REQUEST_PORT);
}

View File

@ -12,6 +12,7 @@ import org.keycloak.testframework.events.EventsSupplier;
import org.keycloak.testframework.events.SysLogServerSupplier;
import org.keycloak.testframework.http.HttpClientSupplier;
import org.keycloak.testframework.http.HttpServerSupplier;
import org.keycloak.testframework.https.CertificatesSupplier;
import org.keycloak.testframework.injection.Supplier;
import org.keycloak.testframework.realm.ClientSupplier;
import org.keycloak.testframework.realm.RealmSupplier;
@ -47,7 +48,8 @@ public class CoreTestFrameworkExtension implements TestFrameworkExtension {
new HttpClientSupplier(),
new HttpServerSupplier(),
new InfinispanExternalServerSupplier(),
new SimpleHttpSupplier()
new SimpleHttpSupplier(),
new CertificatesSupplier()
);
}

View File

@ -3,6 +3,7 @@ package org.keycloak.testframework.admin;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;
import javax.net.ssl.SSLContext;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Supplier;
@ -17,6 +18,13 @@ public class AdminClientFactory {
delegateSupplier = () -> KeycloakBuilder.builder().serverUrl(serverUrl);
}
AdminClientFactory(String serverUrl, SSLContext sslContext) {
delegateSupplier = () ->
KeycloakBuilder.builder()
.serverUrl(serverUrl)
.resteasyClient(Keycloak.getClientProvider().newRestEasyClient(null, sslContext, false));
}
public AdminClientBuilder create() {
return new AdminClientBuilder(this, delegateSupplier.get());
}

View File

@ -1,17 +1,27 @@
package org.keycloak.testframework.admin;
import org.keycloak.testframework.annotations.InjectAdminClientFactory;
import org.keycloak.testframework.https.ManagedCertificates;
import org.keycloak.testframework.injection.InstanceContext;
import org.keycloak.testframework.injection.RequestedInstance;
import org.keycloak.testframework.injection.Supplier;
import org.keycloak.testframework.server.KeycloakServer;
import javax.net.ssl.SSLContext;
public class AdminClientFactorySupplier implements Supplier<AdminClientFactory, InjectAdminClientFactory> {
@Override
public AdminClientFactory getValue(InstanceContext<AdminClientFactory, InjectAdminClientFactory> instanceContext) {
KeycloakServer server = instanceContext.getDependency(KeycloakServer.class);
return new AdminClientFactory(server.getBaseUrl());
if (!server.isTlsEnabled()) {
return new AdminClientFactory(server.getBaseUrl());
} else {
ManagedCertificates managedCert = instanceContext.getDependency(ManagedCertificates.class);
SSLContext sslContext = managedCert.getClientSSLContext();
return new AdminClientFactory(server.getBaseUrl(), sslContext);
}
}
@Override

View File

@ -1,14 +1,18 @@
package org.keycloak.testframework.http;
import org.apache.http.client.HttpClient;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.keycloak.testframework.annotations.InjectHttpClient;
import org.keycloak.testframework.https.ManagedCertificates;
import org.keycloak.testframework.injection.InstanceContext;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.injection.RequestedInstance;
import org.keycloak.testframework.injection.Supplier;
import org.keycloak.testframework.server.KeycloakServer;
import javax.net.ssl.SSLContext;
import java.io.IOException;
public class HttpClientSupplier implements Supplier<HttpClient, InjectHttpClient> {
@ -17,6 +21,19 @@ public class HttpClientSupplier implements Supplier<HttpClient, InjectHttpClient
public HttpClient getValue(InstanceContext<HttpClient, InjectHttpClient> instanceContext) {
HttpClientBuilder builder = HttpClientBuilder.create();
KeycloakServer server = instanceContext.getDependency(KeycloakServer.class);
if (server.isTlsEnabled()) {
ManagedCertificates managedCerts = instanceContext.getDependency(ManagedCertificates.class);
SSLContext sslContext = managedCerts.getClientSSLContext();
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(
sslContext,
SSLConnectionSocketFactory.getDefaultHostnameVerifier()
);
builder.setSSLSocketFactory(sslSocketFactory);
}
if (!instanceContext.getAnnotation().followRedirects()) {
builder.disableRedirectHandling();
}

View File

@ -0,0 +1,30 @@
package org.keycloak.testframework.https;
import org.keycloak.testframework.injection.InstanceContext;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.injection.RequestedInstance;
import org.keycloak.testframework.injection.Supplier;
import org.keycloak.testframework.injection.SupplierOrder;
public class CertificatesSupplier implements Supplier<ManagedCertificates, InjectCertificates> {
@Override
public ManagedCertificates getValue(InstanceContext<ManagedCertificates, InjectCertificates> instanceContext) {
return new ManagedCertificates();
}
@Override
public LifeCycle getDefaultLifecycle() {
return LifeCycle.GLOBAL;
}
@Override
public boolean compatible(InstanceContext<ManagedCertificates, InjectCertificates> a, RequestedInstance<ManagedCertificates, InjectCertificates> b) {
return true;
}
@Override
public int order() {
return SupplierOrder.BEFORE_KEYCLOAK_SERVER;
}
}

View File

@ -0,0 +1,11 @@
package org.keycloak.testframework.https;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectCertificates {
}

View File

@ -0,0 +1,138 @@
package org.keycloak.testframework.https;
import org.apache.http.ssl.SSLContextBuilder;
import org.jboss.logging.Logger;
import org.keycloak.common.crypto.CryptoIntegration;
import org.keycloak.common.crypto.CryptoProvider;
import org.keycloak.common.util.KeystoreUtil;
import org.keycloak.crypto.def.DefaultCryptoProvider;
import javax.net.ssl.SSLContext;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyManagementException;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
public class ManagedCertificates {
private static final Logger LOGGER = Logger.getLogger(ManagedCertificates.class);
private final CryptoProvider cryptoProvider;
private KeyStore serverKeyStore;
private KeyStore clientsTrustStore;
private final static Path KEYSTORES_DIR = Path.of(System.getProperty("java.io.tmpdir"));
private final static Path SERVER_KEYSTORE_FILE_PATH = KEYSTORES_DIR.resolve("kc-testing-server-keystore.jks");
private final static Path CLIENTS_TRUSTSTORE_FILE_PATH = KEYSTORES_DIR.resolve("kc-testing-clients-truststore.jks");
private final static char[] PASSWORD = "password".toCharArray();
private final static String PRV_KEY_ENTRY = "prvKey";
public final static String CERT_ENTRY = "cert";
public ManagedCertificates() throws ManagedCertificatesException {
if (!CryptoIntegration.isInitialised()) {
CryptoIntegration.setProvider(new DefaultCryptoProvider());
}
cryptoProvider = CryptoIntegration.getProvider();
initServerCerts();
}
public String getKeycloakServerKeyStorePath() {
return SERVER_KEYSTORE_FILE_PATH.toString();
}
public String getKeycloakServerKeyStorePassword() {
return String.valueOf(PASSWORD);
}
public KeyStore getClientTrustStore() {
return clientsTrustStore;
}
public X509Certificate getKeycloakServerCertificate() {
try {
return (X509Certificate) serverKeyStore.getCertificate(CERT_ENTRY);
} catch (KeyStoreException e) {
throw new ManagedCertificatesException(e);
}
}
public SSLContext getClientSSLContext() {
try {
return SSLContextBuilder.create()
.loadTrustMaterial(clientsTrustStore, null)
.build();
} catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException e) {
throw new ManagedCertificatesException(e);
}
}
private void initServerCerts() throws ManagedCertificatesException {
try {
serverKeyStore = cryptoProvider.getKeyStore(KeystoreUtil.KeystoreFormat.JKS);
clientsTrustStore = cryptoProvider.getKeyStore(KeystoreUtil.KeystoreFormat.JKS);
if (Files.exists(SERVER_KEYSTORE_FILE_PATH) && Files.exists(CLIENTS_TRUSTSTORE_FILE_PATH)) {
LOGGER.debugv("Existing Server KeyStore files found in {0}", KEYSTORES_DIR);
loadKeyStore(serverKeyStore, SERVER_KEYSTORE_FILE_PATH, PASSWORD);
loadKeyStore(clientsTrustStore, CLIENTS_TRUSTSTORE_FILE_PATH, PASSWORD);
} else {
LOGGER.debugv("Generating Server KeyStore files in {0}", KEYSTORES_DIR);
generateKeystore(serverKeyStore, clientsTrustStore, "localhost");
// store the generated keystore and truststore in a temp folder
try (FileOutputStream fos = new FileOutputStream(SERVER_KEYSTORE_FILE_PATH.toFile())) {
serverKeyStore.store(fos, PASSWORD);
}
try (FileOutputStream fos = new FileOutputStream(CLIENTS_TRUSTSTORE_FILE_PATH.toFile())) {
clientsTrustStore.store(fos, PASSWORD);
}
}
} catch (Exception e) {
throw new ManagedCertificatesException(e);
}
}
private void loadKeyStore(KeyStore keyStore, Path keyStorePath, char[] keyStorePasswd) throws NoSuchAlgorithmException, IOException, CertificateException {
try (FileInputStream fis = new FileInputStream(keyStorePath.toFile())) {
keyStore.load(fis, keyStorePasswd);
}
}
private void generateKeystore(KeyStore keyStore, KeyStore trustStore, String subject) throws NoSuchAlgorithmException, NoSuchProviderException, CertificateException, IOException, KeyStoreException, Exception {
keyStore.load(null);
trustStore.load(null);
KeyPair keyPair = generateKeyPair();
X509Certificate cert = generateX509CertificateCertificate(keyPair, subject);
keyStore.setCertificateEntry(CERT_ENTRY, cert);
trustStore.setCertificateEntry(CERT_ENTRY, cert);
keyStore.setKeyEntry(PRV_KEY_ENTRY, keyPair.getPrivate(), PASSWORD, new X509Certificate[]{cert});
}
private KeyPair generateKeyPair() throws NoSuchAlgorithmException, NoSuchProviderException {
return cryptoProvider.getKeyPairGen("RSA").generateKeyPair();
}
private X509Certificate generateX509CertificateCertificate(KeyPair keyPair, String subject) throws Exception {
// generate a v1 certificate
X509Certificate caCert = cryptoProvider.getCertificateUtils().generateV1SelfSignedCertificate(keyPair, subject);
// generate a v3 certificate
return cryptoProvider.getCertificateUtils().generateV3Certificate(keyPair, keyPair.getPrivate(), caCert, subject);
}
}

View File

@ -0,0 +1,7 @@
package org.keycloak.testframework.https;
public class ManagedCertificatesException extends RuntimeException {
public ManagedCertificatesException(Throwable cause) {
super(cause);
}
}

View File

@ -2,9 +2,10 @@ package org.keycloak.testframework.server;
import org.jboss.logging.Logger;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.infinispan.InfinispanServer;
import org.keycloak.testframework.config.Config;
import org.keycloak.testframework.database.TestDatabase;
import org.keycloak.testframework.https.ManagedCertificates;
import org.keycloak.testframework.infinispan.InfinispanServer;
import org.keycloak.testframework.injection.AbstractInterceptorHelper;
import org.keycloak.testframework.injection.InstanceContext;
import org.keycloak.testframework.injection.LifeCycle;
@ -48,6 +49,12 @@ public abstract class AbstractKeycloakServerSupplier implements Supplier<Keycloa
ServerConfigInterceptorHelper interceptor = new ServerConfigInterceptorHelper(instanceContext.getRegistry());
command = interceptor.intercept(command, instanceContext);
if (command.tlsEnabled()) {
ManagedCertificates managedCert = instanceContext.getDependency(ManagedCertificates.class);
command.option("https-key-store-file", managedCert.getKeycloakServerKeyStorePath());
command.option("https-key-store-password", managedCert.getKeycloakServerKeyStorePassword());
}
command.log().fromConfig(Config.getConfig());
getLogger().info("Starting Keycloak test server");

View File

@ -14,7 +14,6 @@ import java.util.regex.Pattern;
public class DistributionKeycloakServer implements KeycloakServer {
private static final boolean MANUAL_STOP = true;
private static final boolean ENABLE_TLS = false;
private static final boolean RE_CREATE = false;
private static final boolean REMOVE_BUILD_OPTIONS_AFTER_BUILD = false;
private static final int REQUEST_PORT = 8080;
@ -22,6 +21,7 @@ public class DistributionKeycloakServer implements KeycloakServer {
private RawKeycloakDistribution keycloak;
private final boolean debug;
private boolean enableTls = false;
public DistributionKeycloakServer(boolean debug) {
this.debug = debug;
@ -29,7 +29,8 @@ public class DistributionKeycloakServer implements KeycloakServer {
@Override
public void start(KeycloakServerConfigBuilder keycloakServerConfigBuilder) {
keycloak = new RawKeycloakDistribution(false, MANUAL_STOP, ENABLE_TLS, RE_CREATE, REMOVE_BUILD_OPTIONS_AFTER_BUILD, REQUEST_PORT, new LoggingOutputConsumer());
enableTls = keycloakServerConfigBuilder.tlsEnabled();
keycloak = new RawKeycloakDistribution(false, MANUAL_STOP, false, RE_CREATE, REMOVE_BUILD_OPTIONS_AFTER_BUILD, REQUEST_PORT, new LoggingOutputConsumer());
// RawKeycloakDistribution sets "DEBUG_SUSPEND", not "DEBUG" when debug is passed to constructor
if (debug) {
@ -54,12 +55,25 @@ public class DistributionKeycloakServer implements KeycloakServer {
@Override
public String getBaseUrl() {
return "http://localhost:8080";
if (isTlsEnabled()) {
return "https://localhost:8443";
} else {
return "http://localhost:8080";
}
}
@Override
public String getManagementBaseUrl() {
return "http://localhost:9000";
if (isTlsEnabled()) {
return "https://localhost:9000";
} else {
return "http://localhost:9000";
}
}
@Override
public boolean isTlsEnabled() {
return enableTls;
}
private static final class LoggingOutputConsumer implements OutputConsumer {

View File

@ -15,10 +15,12 @@ public class EmbeddedKeycloakServer implements KeycloakServer {
private Keycloak keycloak;
private Path homeDir;
private boolean enableTls = false;
@Override
public void start(KeycloakServerConfigBuilder keycloakServerConfigBuilder) {
Keycloak.Builder builder = Keycloak.builder().setVersion(Version.VERSION);
enableTls = keycloakServerConfigBuilder.tlsEnabled();
for(Dependency dependency : keycloakServerConfigBuilder.toDependencies()) {
builder.addDependency(dependency.getGroupId(), dependency.getArtifactId(), "");
@ -61,11 +63,24 @@ public class EmbeddedKeycloakServer implements KeycloakServer {
@Override
public String getBaseUrl() {
return "http://localhost:8080";
if (isTlsEnabled()) {
return "https://localhost:8443";
} else {
return "http://localhost:8080";
}
}
@Override
public String getManagementBaseUrl() {
return "http://localhost:9001";
if (isTlsEnabled()) {
return "https://localhost:9001";
} else {
return "http://localhost:9001";
}
}
@Override
public boolean isTlsEnabled() {
return enableTls;
}
}

View File

@ -9,4 +9,6 @@ public interface KeycloakServer {
String getBaseUrl();
String getManagementBaseUrl();
boolean isTlsEnabled();
}

View File

@ -31,6 +31,7 @@ public class KeycloakServerConfigBuilder {
private final Set<Path> configFiles = new HashSet<>();
private CacheType cacheType = CacheType.LOCAL;
private boolean externalInfinispan = false;
private boolean tlsEnabled = false;
private KeycloakServerConfigBuilder(String command) {
this.command = command;
@ -99,6 +100,16 @@ public class KeycloakServerConfigBuilder {
return this;
}
public KeycloakServerConfigBuilder tlsEnabled(boolean enabled) {
tlsEnabled = enabled;
return this;
}
public boolean tlsEnabled() {
return tlsEnabled ;
}
public KeycloakServerConfigBuilder cacheConfigFile(String resourcePath) {
try {
Path p = Paths.get(Objects.requireNonNull(getClass().getResource(resourcePath)).toURI());

View File

@ -2,6 +2,7 @@ package org.keycloak.testframework.server;
import io.quarkus.maven.dependency.Dependency;
import javax.net.ssl.SSLException;
import java.net.ConnectException;
import java.net.URL;
import java.nio.file.Path;
@ -10,8 +11,11 @@ import java.util.concurrent.TimeUnit;
public class RemoteKeycloakServer implements KeycloakServer {
private boolean enableTls = false;
@Override
public void start(KeycloakServerConfigBuilder keycloakServerConfigBuilder) {
enableTls = keycloakServerConfigBuilder.tlsEnabled();
if (!verifyRunningKeycloak()) {
printStartupInstructions(keycloakServerConfigBuilder);
waitForStartup();
@ -24,12 +28,25 @@ public class RemoteKeycloakServer implements KeycloakServer {
@Override
public String getBaseUrl() {
return "http://localhost:8080";
if (isTlsEnabled()) {
return "https://localhost:8443";
} else {
return "http://localhost:8080";
}
}
@Override
public String getManagementBaseUrl() {
return "http://localhost:9000";
if (isTlsEnabled()) {
return "https://localhost:9000";
} else {
return "http://localhost:9000";
}
}
@Override
public boolean isTlsEnabled() {
return enableTls;
}
private void printStartupInstructions(KeycloakServerConfigBuilder keycloakServerConfigBuilder) {
@ -70,6 +87,10 @@ public class RemoteKeycloakServer implements KeycloakServer {
return true;
} catch (ConnectException e) {
return false;
} catch (SSLException ignored) {
// if the kc server is running with https, it is not this class' responsibility to check the certificate
// we're just checking that keycloak is running
return true;
} catch (Exception e) {
throw new RuntimeException(e);
}

View File

@ -0,0 +1,97 @@
package org.keycloak.test.examples;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.testframework.annotations.InjectAdminClient;
import org.keycloak.testframework.annotations.InjectHttpClient;
import org.keycloak.testframework.annotations.InjectKeycloakUrls;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.https.InjectCertificates;
import org.keycloak.testframework.https.ManagedCertificates;
import org.keycloak.testframework.oauth.OAuthClient;
import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
import org.keycloak.testframework.server.KeycloakServerConfig;
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
import org.keycloak.testframework.server.KeycloakUrls;
import java.io.IOException;
import java.net.URL;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
@KeycloakIntegrationTest(config = TlsEnabledTest.TlsEnabledServerConfig.class)
public class TlsEnabledTest {
@InjectHttpClient
HttpClient httpClient;
@InjectOAuthClient
OAuthClient oAuthClient;
@InjectAdminClient
Keycloak adminClient;
@InjectCertificates
ManagedCertificates managedCertificates;
@InjectKeycloakUrls
KeycloakUrls keycloakUrls;
@Test
public void testCertSupplier() throws KeyStoreException {
Assertions.assertNotNull(managedCertificates);
KeyStore trustStore = managedCertificates.getClientTrustStore();
Assertions.assertNotNull(trustStore);
X509Certificate cert = managedCertificates.getKeycloakServerCertificate();
Assertions.assertNotNull(cert);
Assertions.assertEquals(cert.getSerialNumber(), ((X509Certificate) trustStore.getCertificate(ManagedCertificates.CERT_ENTRY)).getSerialNumber());
}
@Test
public void testCertDetails() throws CertificateNotYetValidException, CertificateExpiredException {
X509Certificate cert = managedCertificates.getKeycloakServerCertificate();
cert.checkValidity();
Assertions.assertEquals("CN=localhost", cert.getSubjectX500Principal().getName());
Assertions.assertEquals("CN=localhost", cert.getIssuerX500Principal().getName());
}
@Test
public void testHttpClient() throws IOException {
URL baseUrl = keycloakUrls.getBaseUrl();
Assertions.assertEquals("https", baseUrl.getProtocol());
HttpGet req = new HttpGet(baseUrl.toString());
HttpResponse resp = httpClient.execute(req);
Assertions.assertEquals(200, resp.getStatusLine().getStatusCode());
}
@Test
public void testAdminClient() {
adminClient.realm("default");
}
@Test
public void testOAuthClient() {
Assertions.assertTrue(oAuthClient.doWellKnownRequest().getTokenEndpoint().startsWith("https://"));
}
public static class TlsEnabledServerConfig implements KeycloakServerConfig {
@Override
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
return config.tlsEnabled(true);
}
}
}