April 6, 2026

Writing mTLS/Two-Way SSL/Client Certificate Authentication with Spring Boot 4 and HTTP Service Clients

Introduction

In my previous I setup client certification authentication with RestClient https://magnus-k-karlsson.blogspot.com/2026/04/writing-mtlstwo-way-sslclient.html

Here I will use new Spring Boot 4 Rest Client: HTTP Service Clients

Configuration

The configuration of the RestClient is the same.

Java Interface

package se.magnuskkarlsson.clientcert;

import org.springframework.web.service.annotation.GetExchange;

public interface HelloService {

    @GetExchange("/hello")
    public String hello();
}

Use It

    @Autowired
    RestClient restClient;

    private HelloService service;

    // https://docs.spring.io/spring-framework/reference/integration/rest-clients.html#rest-http-service-client
    @BeforeEach
    void setUp() throws Exception {
        // Using RestClient...
        RestClientAdapter adapter = RestClientAdapter.create(restClient);
        HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
        service = factory.createClient(HelloService.class);
    }

    @Test
    void hello() throws Exception {
        System.out.println(service.hello());
    }

Writing mTLS/Two-Way SSL/Client Certificate Authentication with Spring Boot 4 and RestClient

Introduction

In my previous I setup the server side with mLTS https://magnus-k-karlsson.blogspot.com/2026/04/configure-mtlstwo-way-sslclient.html

SSL Bundle

application.properties

# https://docs.spring.io/spring-boot/reference/features/ssl.html
spring.ssl.bundle.jks.mybundle.truststore.location=file:src/test/resources/localhost.p12
spring.ssl.bundle.jks.mybundle.truststore.password=changeit

spring.ssl.bundle.jks.mybundle.keystore.location=file:src/test/resources/localhost.p12
spring.ssl.bundle.jks.mybundle.key.alias=localhost
spring.ssl.bundle.jks.mybundle.keystore.password=changeit
spring.ssl.bundle.jks.mybundle.keystore.type=PKCS12

RestClient Configuration

package se.magnuskkarlsson.clientcert;

import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.core5.util.Timeout;
import org.springframework.boot.restclient.autoconfigure.RestClientSsl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestClient;

@Configuration
public class RestClientConfig {

    // https://dev.to/akdevcraft/never-use-spring-restclient-default-implementation-in-production-100g
    // https://docs.spring.io/spring-boot/api/java/org/springframework/boot/restclient/autoconfigure/RestClientSsl.html
    // https://docs.spring.io/spring-boot/reference/features/ssl.html
    // https://docs.spring.io/spring-boot/appendix/application-properties/index.html
    @Bean
    public RestClient restClient(RestClient.Builder restClientBuilder, RestClientSsl ssl) {
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        connectionManager.setMaxTotal(150); // total connections
        connectionManager.setDefaultMaxPerRoute(50); // per-host connections

        RequestConfig requestConfig = RequestConfig.custom() //
                .setConnectTimeout(Timeout.ofMilliseconds(2000)) // time to establish connection (ms)
                .setResponseTimeout(Timeout.ofMilliseconds(3000)) // time waiting for server response (ms)
                .setConnectionRequestTimeout(Timeout.ofMilliseconds(1000)) // time to wait for connection from pool (ms)
                .build();

        CloseableHttpClient httpClient = HttpClients.custom() //
                .setConnectionManager(connectionManager) //
                .setDefaultRequestConfig(requestConfig) //
                .build();

        return restClientBuilder //
                .requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient)) //
                .apply(ssl.fromBundle("mybundle")) //
                .baseUrl("https://localhost:8443") //
                .build();
    }
}

RestClient Code

    @Autowired
    RestClient restClient;

    @Test
    void hello() {
        // https://dzone.com/articles/spring-boot-32-replace-your-resttemplate-with-rest
        var response = restClient.get() //
                .uri("/hello") //
                .retrieve() //
                .toEntity(String.class);
        System.out.println(response.getBody());
    }

Configure mTLS/Two-Way SSL/Client Certificate Authentication with Spring Boot 4 and Spring Security X.509 Authentication

Introduction

In this blog I'm configure mTLS also called Two-Way SSL or Client Certificate Authentication, with self signed certificate.

Server Certificate (self signed)

$ keytool -genkeypair -alias localhost -dname "cn=localhost, o=Magnus K Karlsson, c=SE" -validity 3650 -keyalg RSA -keysize 2048 -ext ku:c=dig,keyEnc -ext "san=dns:localhost,ip:127.0.0.1" -ext eku=serverAuth -keystore src/test/resources/localhost.p12 -storetype PKCS12 -storepass changeit -keypass changeit

Client Certificate (self signed)

For simplicity we are going to use same certificate.

Server Truststore (Which client certificate are allowed to login)

The server truststore, defines which client CA or client certificate are allowed to login. Here we simply export server certificate, which we use as client certificate as well and import into a new truststore (JKS format).

$ keytool -exportcert -rfc -alias localhost -file src/test/resources/localhost.cert.pem -keystore src/test/resources/localhost.p12 -storepass changeit -storetype PKCS12 -v

$ keytool -importcert -trustcacerts -alias localhost -file src/test/resources/localhost.cert.pem -keystore src/test/resources/truststore-client.jks -storepass changeit -storetype JKS -v

Spring Boot 4 with Spring Security X.509 Authentication

Create a new simple project with Spring Boot 4 and Java 25

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>4.0.5</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <groupId>se.magnuskkarlsson</groupId>
    <artifactId>client-cert</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <properties>
        <java.version>25</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-restclient</artifactId>
        </dependency>

        <!-- Apache HttpClient 5 (for advanced pooling, timeouts, etc.) -->
        <dependency>
            <groupId>org.apache.httpcomponents.client5</groupId>
            <artifactId>httpclient5</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-restclient-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webmvc-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Create simple REST Controller

package se.magnuskkarlsson.clientcert;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello World!";
    }
}

Now create Spring Security Configuration class with X.509 Authentication.

package se.magnuskkarlsson.clientcert;

import java.security.cert.X509Certificate;

import javax.security.auth.x500.X500Principal;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.preauth.x509.X509PrincipalExtractor;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final Log log = LogFactory.getLog(getClass());

    // https://docs.spring.io/spring-security/reference/servlet/authentication/x509.html
    @Bean
    DefaultSecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http //
                .authorizeHttpRequests((exchanges) -> exchanges //
                        .anyRequest().authenticated() //
                ) //
                .x509((x509) -> x509 //
                        .x509PrincipalExtractor(x509PrincipalExtractor()) //
                ) //
                .userDetailsService(userDetailsService());
        return http.build();
    }

    @Bean
    X509PrincipalExtractor x509PrincipalExtractor() {
        // org.springframework.security.web.authentication.preauth.x509.SubjectX500PrincipalExtractor
        return new X509PrincipalExtractor() {
            @Override
            public Object extractPrincipal(X509Certificate cert) {
                X500Principal principal = cert.getSubjectX500Principal();
                return principal.toString();
            }
        };
    }

    @Bean
    UserDetailsService userDetailsService() {
        return new UserDetailsService() {
            @Override
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                log.info("username='" + username + "'");
                // accept all mTLS verified client certificate, here you would normally lookup user from storage
                return new User(username, "", AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER"));
            }
        };
    }
}

Finally configure the application.

spring.application.name=client-cert

server.port=8443
server.ssl.enabled=true

server.ssl.key-store=src/test/resources/localhost.p12
server.ssl.key-store-password=changeit

server.ssl.trust-store=src/test/resources/truststore-client.jks
server.ssl.trust-store-password=changeit
server.ssl.client-auth=need

#logging.level.org.springframework.security=TRACE

Test

Start application with

$ mvn spring-boot:run

To test with curl you need to export client certifite to PEM

$ openssl pkcs12 -in src/test/resources/localhost.p12 -out src/test/resources/localhost.key.pem -nodes

$ cp src/test/resources/localhost.key.pem src/test/resources/localhost.cert.pem
$ curl -i --request GET \
     --url     https://localhost:8443/hello?name=FOO \
     --cacert  src/test/resources/localhost.cert.pem \
     --cert    src/test/resources/localhost.cert.pem \
     --key     src/test/resources/localhost.key.pem