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