Java HTTPS Requests with Client Certificates (Two-Way Authentication)

Two-way TLS (also called mutual TLS, or mTLS) is an authentication mechanism where the server verifies the client's identity in addition to the client verifying the server's. It is common for B2B APIs, banking endpoints, and internal microservices.

This guide shows how to send mTLS requests from Java using the built-in HttpClient introduced in Java 11.

What you need

  • A client keystore (usually a .p12 or .pfx file) containing your client certificate and its private key.
  • A truststore containing the server's CA certificate, unless the CA is already in the JVM's default truststore.
  • The password for the keystore (and optionally the key alias if there are several entries).
  • Java 11 or later.

Step 1: load the keystore

import java.io.FileInputStream;
import java.security.KeyStore;

KeyStore clientKs = KeyStore.getInstance("PKCS12");
try (FileInputStream fis = new FileInputStream("/path/to/client.p12")) {
    clientKs.load(fis, "changeit".toCharArray());
}

If your keystore is a legacy .jks file, replace "PKCS12" with "JKS". PKCS12 is the modern default and should be preferred.

Step 2: build the SSLContext

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;

// Key manager handles our client certificate
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(clientKs, "changeit".toCharArray());

// Trust manager — here we load a custom truststore; skip if JVM default is fine
KeyStore trustKs = KeyStore.getInstance("PKCS12");
try (FileInputStream fis = new FileInputStream("/path/to/truststore.p12")) {
    trustKs.load(fis, "changeit".toCharArray());
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustKs);

// Wire them together
SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);

Step 3: create an HttpClient with the SSLContext

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

HttpClient client = HttpClient.newBuilder()
    .sslContext(sslContext)
    .connectTimeout(Duration.ofSeconds(10))
    .build();

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/secure/endpoint"))
    .header("Accept", "application/json")
    .GET()
    .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.body());

Sending a POST with a body

String json = "{\"action\":\"ping\"}";

HttpRequest post = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/ops"))
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(json))
    .build();

HttpResponse<String> resp = client.send(post, HttpResponse.BodyHandlers.ofString());

Alternative: JVM-wide system properties

If you prefer not to build an SSLContext manually, you can tell the JVM where to find the keystore and truststore via flags:

java \
  -Djavax.net.ssl.keyStore=/path/to/client.p12 \
  -Djavax.net.ssl.keyStorePassword=changeit \
  -Djavax.net.ssl.keyStoreType=PKCS12 \
  -Djavax.net.ssl.trustStore=/path/to/truststore.p12 \
  -Djavax.net.ssl.trustStorePassword=changeit \
  -jar app.jar

This is convenient for quick tests but inflexible in apps that need to talk to multiple TLS endpoints with different certificates.

Common pitfalls

  • Wrong algorithm: KeyStore.getInstance("JKS") will fail silently on a .p12 file.
  • Password mismatch: the keystore password and the private key password can differ in older .jks files.
  • Multiple aliases: if the .p12 contains several keys, use a custom X509KeyManager to pick the right one by alias.
  • Expired cert: always check the validity window — keytool -list -v -keystore client.p12.
  • Hostname mismatch: if the server's certificate CN/SAN does not match the URL, the handshake fails. Don't disable hostname verification in production.

Debugging the handshake

When things go wrong, Java's built-in TLS debug output is invaluable:

java -Djavax.net.debug=ssl:handshake:verbose -jar app.jar

The output is verbose but tells you exactly which certificate was presented, which CA chain was validated, and which cipher was negotiated.

For anything beyond the JDK's HttpClient, libraries like OkHttp and Apache HttpClient 5 offer similar mTLS support with slightly different APIs but the same underlying concepts.