diff --git a/test-framework/clustering/src/main/java/org/keycloak/testframework/server/ClusteredKeycloakServer.java b/test-framework/clustering/src/main/java/org/keycloak/testframework/server/ClusteredKeycloakServer.java index c3af53d1f14..b3ec5cad0d7 100644 --- a/test-framework/clustering/src/main/java/org/keycloak/testframework/server/ClusteredKeycloakServer.java +++ b/test-framework/clustering/src/main/java/org/keycloak/testframework/server/ClusteredKeycloakServer.java @@ -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); } diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/CoreTestFrameworkExtension.java b/test-framework/core/src/main/java/org/keycloak/testframework/CoreTestFrameworkExtension.java index 69436d157ac..19b38dc283f 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/CoreTestFrameworkExtension.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/CoreTestFrameworkExtension.java @@ -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() ); } diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/admin/AdminClientFactory.java b/test-framework/core/src/main/java/org/keycloak/testframework/admin/AdminClientFactory.java index c53384fb2b6..e7287e0968d 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/admin/AdminClientFactory.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/admin/AdminClientFactory.java @@ -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()); } diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/admin/AdminClientFactorySupplier.java b/test-framework/core/src/main/java/org/keycloak/testframework/admin/AdminClientFactorySupplier.java index c2c51fc8453..124d5522fdf 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/admin/AdminClientFactorySupplier.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/admin/AdminClientFactorySupplier.java @@ -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 { @Override public AdminClientFactory getValue(InstanceContext 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 diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/http/HttpClientSupplier.java b/test-framework/core/src/main/java/org/keycloak/testframework/http/HttpClientSupplier.java index ef62a5a776a..7ffaea65b3c 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/http/HttpClientSupplier.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/http/HttpClientSupplier.java @@ -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 { @@ -17,6 +21,19 @@ public class HttpClientSupplier implements Supplier 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(); } diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/https/CertificatesSupplier.java b/test-framework/core/src/main/java/org/keycloak/testframework/https/CertificatesSupplier.java new file mode 100644 index 00000000000..0320c5aad7a --- /dev/null +++ b/test-framework/core/src/main/java/org/keycloak/testframework/https/CertificatesSupplier.java @@ -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 { + + @Override + public ManagedCertificates getValue(InstanceContext instanceContext) { + return new ManagedCertificates(); + } + + @Override + public LifeCycle getDefaultLifecycle() { + return LifeCycle.GLOBAL; + } + + @Override + public boolean compatible(InstanceContext a, RequestedInstance b) { + return true; + } + + @Override + public int order() { + return SupplierOrder.BEFORE_KEYCLOAK_SERVER; + } +} diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/https/InjectCertificates.java b/test-framework/core/src/main/java/org/keycloak/testframework/https/InjectCertificates.java new file mode 100644 index 00000000000..8b5a5c29336 --- /dev/null +++ b/test-framework/core/src/main/java/org/keycloak/testframework/https/InjectCertificates.java @@ -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 { +} diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/https/ManagedCertificates.java b/test-framework/core/src/main/java/org/keycloak/testframework/https/ManagedCertificates.java new file mode 100644 index 00000000000..4b4e7c39f54 --- /dev/null +++ b/test-framework/core/src/main/java/org/keycloak/testframework/https/ManagedCertificates.java @@ -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); + } +} diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/https/ManagedCertificatesException.java b/test-framework/core/src/main/java/org/keycloak/testframework/https/ManagedCertificatesException.java new file mode 100644 index 00000000000..cd85da1a0a0 --- /dev/null +++ b/test-framework/core/src/main/java/org/keycloak/testframework/https/ManagedCertificatesException.java @@ -0,0 +1,7 @@ +package org.keycloak.testframework.https; + +public class ManagedCertificatesException extends RuntimeException { + public ManagedCertificatesException(Throwable cause) { + super(cause); + } +} diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/server/AbstractKeycloakServerSupplier.java b/test-framework/core/src/main/java/org/keycloak/testframework/server/AbstractKeycloakServerSupplier.java index 1371508a769..7073904f19b 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/server/AbstractKeycloakServerSupplier.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/server/AbstractKeycloakServerSupplier.java @@ -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 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()); diff --git a/test-framework/core/src/main/java/org/keycloak/testframework/server/RemoteKeycloakServer.java b/test-framework/core/src/main/java/org/keycloak/testframework/server/RemoteKeycloakServer.java index 8d3d9fd6eab..06f9ef8ce26 100644 --- a/test-framework/core/src/main/java/org/keycloak/testframework/server/RemoteKeycloakServer.java +++ b/test-framework/core/src/main/java/org/keycloak/testframework/server/RemoteKeycloakServer.java @@ -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); } diff --git a/test-framework/examples/tests/src/test/java/org/keycloak/test/examples/TlsEnabledTest.java b/test-framework/examples/tests/src/test/java/org/keycloak/test/examples/TlsEnabledTest.java new file mode 100644 index 00000000000..0f1934062b4 --- /dev/null +++ b/test-framework/examples/tests/src/test/java/org/keycloak/test/examples/TlsEnabledTest.java @@ -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); + } + } +}