diff --git a/api-catalog-ui/frontend/package-lock.json b/api-catalog-ui/frontend/package-lock.json index e997dc725a..5d9dbfe1e0 100644 --- a/api-catalog-ui/frontend/package-lock.json +++ b/api-catalog-ui/frontend/package-lock.json @@ -115,6 +115,7 @@ "source-map-explorer": "2.5.3", "start-server-and-test": "2.0.8", "tmpl": "1.0.5", + "undici": "6.19.8", "yaml": "2.6.0" }, "engines": { @@ -29531,10 +29532,11 @@ "dev": true }, "node_modules/undici": { - "version": "6.20.1", - "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/undici/-/undici-6.20.1.tgz", - "integrity": "sha512-AjQF1QsmqfJys+LXfGTNum+qw4S88CojRInG/6t31W/1fk6G59s92bnAvGz5Cmur+kQv2SURXEvvudLmbrE8QA==", + "version": "6.19.8", + "resolved": "https://zowe.jfrog.io/artifactory/api/npm/npm-org/undici/-/undici-6.19.8.tgz", + "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=18.17" } diff --git a/api-catalog-ui/frontend/package.json b/api-catalog-ui/frontend/package.json index bd22607bc9..224fb5b033 100644 --- a/api-catalog-ui/frontend/package.json +++ b/api-catalog-ui/frontend/package.json @@ -135,7 +135,8 @@ "source-map-explorer": "2.5.3", "start-server-and-test": "2.0.8", "tmpl": "1.0.5", - "yaml": "2.6.0" + "yaml": "2.6.0", + "undici": "6.19.8" }, "overrides": { "nth-check": "2.1.1", diff --git a/client-cert-auth-sample/build.gradle b/client-cert-auth-sample/build.gradle new file mode 100644 index 0000000000..deba9403f0 --- /dev/null +++ b/client-cert-auth-sample/build.gradle @@ -0,0 +1,18 @@ +plugins { + id 'java' +} + +group = 'org.zowe.apiml' +version = '3.0.39-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + implementation libs.http.client5 +} + +test { + useJUnitPlatform() +} diff --git a/client-cert-auth-sample/src/main/java/org/zowe/apiml/Main.java b/client-cert-auth-sample/src/main/java/org/zowe/apiml/Main.java new file mode 100644 index 0000000000..ff94c76ace --- /dev/null +++ b/client-cert-auth-sample/src/main/java/org/zowe/apiml/Main.java @@ -0,0 +1,81 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.io.BasicHttpClientConnectionManager; +import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; +import org.apache.hc.core5.ssl.SSLContextBuilder; + +import java.io.File; +import java.io.FileInputStream; +import java.security.KeyStore; +import java.security.cert.Certificate; + +public class Main { + + private static final String API_URL = "https://localhost:8080/gateway/api/v1/auth/login"; // Replace with your API URL + private static final String CLIENT_CERT_PATH = "../keystore/client_cert/client-certs.p12"; // Replace with your client cert path + private static final String CLIENT_CERT_PASSWORD = "password"; // Replace with your cert password + private static final String CLIENT_CERT_ALIAS = "apimtst"; // Replace with your signed client cert alias + private static final String PRIVATE_KEY_ALIAS = "apimtst"; // Replace with your private key alias + + + public static void main(String[] args) { + try { + // Load the keystore containing the client certificate + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + try (FileInputStream keyStoreStream = new FileInputStream(new File(CLIENT_CERT_PATH))) { + keyStore.load(keyStoreStream, CLIENT_CERT_PASSWORD.toCharArray()); + } + + var key = keyStore.getKey(PRIVATE_KEY_ALIAS, CLIENT_CERT_PASSWORD.toCharArray()); // Load private key from original keystore + var cert = keyStore.getCertificate(CLIENT_CERT_ALIAS); // Load signed certificate from original keystore + + // Create new keystore + var newKeyStore = KeyStore.getInstance("PKCS12"); + newKeyStore.load(null); + newKeyStore.setKeyEntry(PRIVATE_KEY_ALIAS, key, CLIENT_CERT_PASSWORD.toCharArray(), new Certificate[]{cert}); // Create an entry with private key + signed certificate + + // Create SSL context with the client certificate + var sslContext = new SSLContextBuilder().loadTrustMaterial((chain, type) -> true) + .loadKeyMaterial(newKeyStore, CLIENT_CERT_PASSWORD.toCharArray()).build(); + var sslsf = new DefaultClientTlsStrategy(sslContext); + + + var connectionManager = BasicHttpClientConnectionManager.create((s) -> sslsf); + + var clientBuilder = HttpClientBuilder.create().setConnectionManager(connectionManager); + + try (var httpClient = clientBuilder.build()) { + + // Create a POST request + var httpPost = new HttpPost(API_URL); + + // Execute the request + var response = httpClient.execute(httpPost, res -> res); + + // Print the response status + System.out.println("Response Code: " + response.getCode()); + + // Print headers + var headers = response.getHeaders(); + for (var header : headers) { + System.out.println("Key : " + header.getName() + + " ,Value : " + header.getValue()); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/client-cert-auth-sample/src/test/java/org/zowe/apiml/MainTest.java b/client-cert-auth-sample/src/test/java/org/zowe/apiml/MainTest.java new file mode 100644 index 0000000000..05278547a6 --- /dev/null +++ b/client-cert-auth-sample/src/test/java/org/zowe/apiml/MainTest.java @@ -0,0 +1,110 @@ +/* + * This program and the accompanying materials are made available under the terms of the + * Eclipse Public License v2.0 which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v20.html + * + * SPDX-License-Identifier: EPL-2.0 + * + * Copyright Contributors to the Zowe Project. + */ + +package org.zowe.apiml; + +import com.sun.net.httpserver.HttpsConfigurator; +import com.sun.net.httpserver.HttpsExchange; +import com.sun.net.httpserver.HttpsParameters; +import com.sun.net.httpserver.HttpsServer; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManagerFactory; +import java.io.FileInputStream; +import java.net.InetSocketAddress; +import java.security.KeyStore; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class MainTest { + + static HttpsServer httpServer; + static AssertionError error; + + @BeforeAll + static void setup() throws Exception { + var inetAddress = new InetSocketAddress("127.0.0.1", 8080); + httpServer = HttpsServer.create(inetAddress, 0); + + var sslContext = SSLContext.getInstance("TLS"); + var ks = KeyStore.getInstance("PKCS12"); + try (var fis = new FileInputStream("../keystore/localhost/localhost.keystore.p12")) { + ks.load(fis, "password".toCharArray()); + } + var km = KeyManagerFactory.getInstance("SunX509"); + km.init(ks, "password".toCharArray()); + var ts = KeyStore.getInstance("PKCS12"); + try (var fis = new FileInputStream("../keystore/localhost/localhost.truststore.p12")) { + ts.load(fis, "password".toCharArray()); + } + var tm = TrustManagerFactory.getInstance("SunX509"); + tm.init(ts); + + sslContext.init(km.getKeyManagers(), tm.getTrustManagers(), null); + + var httpsConfigurator = new TestHttpsConfigurator(sslContext); + httpServer.setHttpsConfigurator(httpsConfigurator); + httpServer.createContext("/gateway/api/v1/auth/login", exchange -> { + + exchange.sendResponseHeaders(204, 0); + var clientCert = ((HttpsExchange) exchange).getSSLSession().getPeerCertificates(); + try { + // client certificate must be present at this stage + assertNotNull(clientCert); + } catch (AssertionError e) { + error = e; + } + exchange.close(); + }); + httpServer.start(); + + } + + @AfterAll + static void tearDown() { + + httpServer.stop(0); + if (error != null) { + throw error; + } + } + + @Test + void givenHttpsRequestWithClientCertificate_thenPeerCertificateMustBeAvailable() { + // Assertion is done on the server to make sure that client certificate was delivered. + // Assertion error is then rethrown in the tear down method in case certificate was not present. + Main.main(null); + } + + static class TestHttpsConfigurator extends HttpsConfigurator { + /** + * Creates a Https configuration, with the given {@link SSLContext}. + * + * @param context the {@code SSLContext} to use for this configurator + * @throws NullPointerException if no {@code SSLContext} supplied + */ + public TestHttpsConfigurator(SSLContext context) { + super(context); + } + + @Override + public void configure(HttpsParameters params) { + var parms = getSSLContext().getDefaultSSLParameters(); + parms.setNeedClientAuth(true); + params.setWantClientAuth(true); + params.setSSLParameters(parms); + } + } + +} diff --git a/settings.gradle b/settings.gradle index 6f95c3f41c..b9079d3afb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -55,4 +55,5 @@ include 'apiml-sample-extension' include 'apiml-sample-extension-package' include 'apiml-extension-loader' include 'zowe-cli-id-federation-plugin' +include 'client-cert-auth-sample'