Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add java sample app to authenticate client certificate #4009

Open
wants to merge 2 commits into
base: v2.x.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions client-cert-auth-sample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Run Client Certificate Authentication Sample

This project is a Java-based client that performs authentication using a client certificate.
It utilizes the Apache HTTP Client to send an HTTPS request with client certificate authentication.

## Prerequisites

1. Java 8
2. A valid trusted client certificate stored in a PKCS12 keystore (.p12 or .pfx file)

## Running the application

1. Build the client-cert-auth-sample

```shell
./gradlew :client-cert-auth-sample:build
```

2. Export the following environment variables:

**macOS:**

```shell
export API_URL="<API_URL>"
export CLIENT_CERT_PATH="<CLIENT_CERT_PATH>"
export CLIENT_CERT_PASSWORD="<CLIENT_CERT_PASSWORD>"
export CLIENT_CERT_ALIAS="<CLIENT_CERT_ALIAS>"
export PRIVATE_KEY_ALIAS="<PRIVATE_KEY_ALIAS>"
```
**Windows:**

```shell
set API_URL="<API_URL>"
set CLIENT_CERT_PATH="<CLIENT_CERT_PATH>"
set CLIENT_CERT_PASSWORD="<CLIENT_CERT_PASSWORD>"
set CLIENT_CERT_ALIAS="<CLIENT_CERT_ALIAS>"
set PRIVATE_KEY_ALIAS="<PRIVATE_KEY_ALIAS>"
```

3. Run the JAR located inside the `build/libs` folder with the SSL debug argument:
```shell
java -jar client-cert-auth-sample.jar -Djavax.net.debug=all
```

This will output detailed information about the SSL handshake and certificate validation process.
30 changes: 30 additions & 0 deletions client-cert-auth-sample/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
plugins {
id 'java'
}

group = 'org.zowe.apiml'
version = '3.0.39-SNAPSHOT'

repositories {
mavenCentral()
}

dependencies {
implementation libs.http.client
implementation group: 'commons-logging', name: 'commons-logging', version: '1.3.5'
}

test {
useJUnitPlatform()
}

jar {
manifest {
attributes 'Main-Class': 'org.zowe.apiml.Main'
}
from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

97 changes: 97 additions & 0 deletions client-cert-auth-sample/src/main/java/org/zowe/apiml/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* 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.http.Header;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.apache.http.ssl.SSLContextBuilder;

import javax.net.ssl.SSLContext;
import java.io.FileInputStream;
import java.security.Key;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.util.Optional;

public class Main {

private static final String API_URL = Optional.ofNullable(System.getenv("API_URL")).orElse("https://localhost:10010") + "/gateway/api/v1/auth/login"; // Replace with your API URL
private static final String CLIENT_CERT_PATH = Optional.ofNullable(System.getenv("CLIENT_CERT_PATH")).orElse("../keystore/client_cert/client-certs.p12"); // Replace with your client cert path
private static final String CLIENT_CERT_PASSWORD = Optional.ofNullable(System.getenv("CLIENT_CERT_PASSWORD")).orElse("password"); // Replace with your cert password
private static final String CLIENT_CERT_ALIAS = Optional.ofNullable(System.getenv("CLIENT_CERT_ALIAS")).orElse("user"); // Replace with your signed client cert alias
private static final String PRIVATE_KEY_ALIAS = Optional.ofNullable(System.getenv("PRIVATE_KEY_ALIAS")).orElse("user"); // 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(CLIENT_CERT_PATH)) {
keyStore.load(keyStoreStream, CLIENT_CERT_PASSWORD.toCharArray());
}

Key key = keyStore.getKey(PRIVATE_KEY_ALIAS, CLIENT_CERT_PASSWORD.toCharArray()); // Load private key from original keystore
Certificate cert = keyStore.getCertificate(CLIENT_CERT_ALIAS); // Load signed certificate from original keystore

// Create new keystore
KeyStore 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
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial((chain, type) -> true)
.loadKeyMaterial(newKeyStore, CLIENT_CERT_PASSWORD.toCharArray()).build();
ConnectionSocketFactory connectionSocketFactory = new SSLConnectionSocketFactory(sslContext);

RegistryBuilder<ConnectionSocketFactory> socketFactoryRegistryBuilder = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", connectionSocketFactory);
Registry<ConnectionSocketFactory> socketFactoryRegistry = socketFactoryRegistryBuilder.build();

try (BasicHttpClientConnectionManager connectionManager = new BasicHttpClientConnectionManager(socketFactoryRegistry)) {

HttpClientBuilder clientBuilder = HttpClientBuilder.create().setConnectionManager(connectionManager);

try (CloseableHttpClient httpClient = clientBuilder.build()) {

// Create a POST request
HttpPost httpPost = new HttpPost(API_URL);

// Execute the request
CloseableHttpResponse response = httpClient.execute(httpPost);

// Print the response status
System.out.println("Response Code: " + response.getStatusLine().getStatusCode());

// Print headers
Header[] headers = response.getAllHeaders();
for (Header header : headers) {
System.out.println("Key : " + header.getName() + " ,Value : " + header.getValue());
}
}
}

} catch (Exception e) {
e.printStackTrace();
}
}
}

112 changes: 112 additions & 0 deletions client-cert-auth-sample/src/test/java/org/zowe/apiml/MainTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* 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.SSLParameters;
import javax.net.ssl.TrustManagerFactory;
import java.io.FileInputStream;
import java.net.InetSocketAddress;
import java.security.KeyStore;
import java.security.cert.Certificate;

import static org.junit.jupiter.api.Assertions.assertNotNull;

class MainTest {

static HttpsServer httpServer;
static AssertionError error;

@BeforeAll
static void setup() throws Exception {
InetSocketAddress inetAddress = new InetSocketAddress("127.0.0.1", 8080);
httpServer = HttpsServer.create(inetAddress, 0);

SSLContext sslContext = SSLContext.getInstance("TLS");
KeyStore keyStore = KeyStore.getInstance("PKCS12");
try (FileInputStream fileInputStream = new FileInputStream("../keystore/localhost/localhost.keystore.p12")) {
keyStore.load(fileInputStream, "password".toCharArray());
}
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(keyStore, "password".toCharArray());
KeyStore trustStore = KeyStore.getInstance("PKCS12");
try (FileInputStream fileInputStream = new FileInputStream("../keystore/localhost/localhost.truststore.p12")) {
trustStore.load(fileInputStream, "password".toCharArray());
}
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
trustManagerFactory.init(trustStore);

sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);

TestHttpsConfigurator httpsConfigurator = new TestHttpsConfigurator(sslContext);
httpServer.setHttpsConfigurator(httpsConfigurator);
httpServer.createContext("/gateway/api/v1/auth/login", exchange -> {

exchange.sendResponseHeaders(204, 0);
Certificate[] 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) {
SSLParameters parms = getSSLContext().getDefaultSSLParameters();
parms.setNeedClientAuth(true);
params.setWantClientAuth(true);
params.setSSLParameters(parms);
}
}

}
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,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'

Loading