April 6, 2026

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

No comments: