Last Updated on 18/10/2024 by Grant Little
Overview
This article focus’ on mTLS Client Authentication with Spring Boot and how to best implement it and what the various options mean in reality.
Security is paramount in modern computer systems. There are plenty of malicious actors ready to pounce out there should they see any potential for an attack.
Therefore it is important you maintain strict control over who has access to your applications. That takes into consideration authentication, the client is who they say they are and authorization once the client is authenticated, what have they actually got access to.
One mechanism of implementing authentication is via SSL/TLS Client Authentication (or commonly known as mTLS – mutual TLS). It can be used to identify a user on a browser via a private certificate installed on their machine. However, these days it is more commonly used to authenticate between backend services, especially in the microservices world. Note SSL/TLS client authentication is just that. It only provides authentication, not authorization.
Mutual TLS assumes the both parties involved in the communication channel have a certificate. The server has a certificate that is trusted by the client (like a typical website certificate) and the client has a certificate trusted by the server.
Assumptions
I’m going to assume that you have a basic grasp on how SSL/TLS client authentication works. There are plenty of resources on the internet already covering this, but just in case here are a few options
- https://www.cloudflare.com/en-au/learning/access-management/what-is-mutual-tls/
- https://www.youtube.com/watch?v=b38k2GiLDdc
I’m also going to assume for this article that you have access to the resources to create the correct certificates to enable this bi-directional trust. There are plenty of articles out there on how to do it. The world doesn’t need another one. However, here is a shell script that you can use as a starting point.
Instead I’m going to focus on how you use these certificates within a Spring Boot Server and Client to enable this bi-directional trust.
Configuration
In Spring Boot there are a number of important configuration properties that must be set. In YAML form they are similar to the following:-
Server
spring:
ssl:
bundle:
jks:
server:
keystore:
location: "classpath:certs/server.p12"
password: "password"
type: "PKCS12"
truststore:
location: "classpath:certs/mtls-trusts.jks"
password: "password"
server:
ssl:
client-auth: want
enabled: true
bundle: "server"
YAML- server.ssl.enabled: SSL/TLS must be enabled by the webserver for mTLS to work
- server.ssl.bundle: This points to the certificate information the server should use to secure the endpoints, but also provides a trust store which will contain either the CA the clients certificates are signed by, or the client certificate itself. Note: The value “server” is effectively a link to the “spring.ssl.bundle.jks.server” property also defined here. The “server” is just an abstract name and can be called anything, as long as the “server.ssl.bundle” property is correctly set to the same name
- server.ssl.client-auth: This property enables client authentication with a value of “need” or “want”. If it is set to “none” then client authentication is disabled. This is the same as not defining a value for the property at all.
Client
spring:
ssl:
bundle:
jks:
client:
keystore:
location: "classpath:certs/client.p12"
password: "password"
type: "PKCS12"
truststore:
location: "classpath:certs/client.jks"
password: "password"
YAMLFor the client we just need to define the SSL bundle and then we will refer to the bundle name in our code.
Note: is it possible to not explicitly provide the trust store for the client. However the CA that the server certificate was signed by must then exist in the default JRE trust store, typically located in the “<jre_home>/lib/security/cacerts” file.
Client Authentication Modes
The property “server.ssl.client-auth” allows 3 possible values. The following table defines the expected behaviour for each of those values when
- The URL being requested requires authentication
- The URL being requested does not require authentication
Then for both of those under 3 possible circumstances
- Client provides a valid certificate trusted by the server (Valid)
- Client provides a certificate not trusted by the server (Invalid)
- Client fails to provide a certificate (None)
URL Requires Authentication |
URL Does not require Authentication |
|||||
Client Certificate |
||||||
Mode |
Valid |
Invalid |
None |
Valid |
Invalid |
None |
none |
N/A |
HTTP 401 Forbidden |
HTTP 401 Forbidden |
N/A |
Accessible |
Accessible |
want |
Accessible |
HTTP 401 Forbidden |
HTTP 401 Forbidden |
Accessible |
Accessible |
Accessible |
need |
Accessible |
SSL Handshake Failure |
SSL Handshake Failure |
Accessible |
SSL Handshake Failure |
SSL Handshake Failure |
Requirements
For the server
- A server certificate signed by a trusted certificate authority (CA) (we’ll refer to this as server.crt)
- The full trust chain for the CA. In our case it’s a single CA (we’ll refer to this as server-ca.crt)
- The private key for the server certificate (we’ll refer to this as server.key)
- NOTE: All of the above can be contained in a PKCS12 file (we’ll refer to this as server.p12)
- An organisational CA explicitly used for signing client certificates. Generally this CA would be internal to the organisation. This needs to be stored in a JKS file. We will call this the the mtls-trusts.jks
For the client
- A client certificate signed by the organisational CA (5 above) and the associated private key (we’ll refer to this as client.p12)
- The server CA (2 above) stored in a Java Key Store. (we’ll refer to this as client.jks). If you are using a CA from a public provider, you likely don’t need this as it will be in your default Java Trust Store
Code
Below are examples of configuring a Spring Boot application to enable SSL/TLS client authentication when using both WebFlux (reactive) or WebMvc (blocking). I have provided comments in the code at points of interest.
If you need help setting up a spring project then have a look at this article on using the Spring Initializer
Server
WebFlux
@SpringBootApplication
@EnableWebFluxSecurity
class WebFluxX509Application {
// Need to define a SecurityWebFilterChain so we can configure the security we need
@Bean
@Throws(Exception::class)
fun securityFilterChain(
http: ServerHttpSecurity
): SecurityWebFilterChain {
return http
// Enable redirect to https protocol
.redirectToHttps {}
// Enable SSL/TLS Client authentication using x509 certificates
.x509 { }
// Configure which requests require authentication
.authorizeExchange { requests ->
requests
// Any context path matching /** pattern does not need to be authenticated
.pathMatchers("/public/**").permitAll()
// All other requests need to be authenticated
.anyExchange().authenticated()
}
.build()
}
}
KotlinWebMvc
@SpringBootApplication
@EnableWebSecurity
class WebMvcX509Application {
// Need to define a SecurityFilterChain so we can configure the security we need
@Bean
@Throws(Exception::class)
fun securityFilterChain(
http: HttpSecurity
): SecurityFilterChain {
http
// Enable redirect to https protocol
.requiresChannel { it.anyRequest().requiresSecure() }
// Enable SSL/TLS Client authentication using x509 certificates
.x509 { }
// Configure which requests require authentication
.authorizeHttpRequests { requests ->
requests
// Any context path matching /** pattern does not need to be authenticated
.requestMatchers("/public/**").permitAll()
// All other requests need to be authenticated
.anyRequest().authenticated()
}
return http.build()
}
}
KotlinClient
WebFlux
@Component
class Client(
private val webClientSsl: WebClientSsl,
) {
// Create a rest template that will use the "client" SSL bundle from application.yml
private val webClient = WebClient.builder()
.apply { webClientSsl.fromBundle("client") }
.build()
fun makeRequest() {
webClient
.get()
.uri("https://localhost/private")
.retrieve()
.bodyToMono(String::class.java)
.subscribe { println(it) }
}
}
KotlinWebMvc
@Component
class Client(
restTemplateBuilder: RestTemplateBuilder,
sslBundles: SslBundles
) {
// Create a rest template that will use the "client" SSL bundle from application.yml
val restTemplate = restTemplateBuilder
.setSslBundle(sslBundles.getBundle("client"))
.build()
fun makeRequest() {
restTemplate
.getForEntity("https://localhost/private", String::class.java)
.body
.let { println(it) }
}
}
KotlinAuthorization
mTLS Client Authentication with Spring Boot only considers the authentication side of the equation. However, we sometimes also want to define finer grained control over what actions and resources an authenticated user can interact with. For that we need to use Authorization.
Spring Boot enables this by matching some attributes of the certificate (typically the Common Name or CN) to a user stored some where in the system. That then allows control over what permissions, roles or authorities that user has.
In the following examples I have created an in memory user details store, with the available users hard coded. Obviously you would want to replace this with some other implementation of UserDetailsManager (or reactive equivalent) for example JdbcUserDetailsManager. In many circumstances you would provide your own implementation of this interface.
In the following examples I have extended our configuration further to demonstrate the interaction between authentication and authorization.
WebFlux
@SpringBootApplication
@EnableWebFluxSecurity
class WebFluxX509Application {
// Need to define a SecurityWebFilterChain so we can configure the security we need
@Bean
@Throws(Exception::class)
fun securityFilterChain(
http: ServerHttpSecurity,
// Inject the reactive user details service
userDetailsService: ReactiveUserDetailsService,
): SecurityWebFilterChain {
return http
// Enable redirect to https protocol
.redirectToHttps {}
// Enable SSL/TLS Client authentication using x509 certificates
.x509 {
// Ability to extract the user details from the authenticated certificate's common name (CN)
it.principalExtractor(SubjectDnX509PrincipalExtractor().apply {
//Extract the common name (CN) from the certificate. Used to create a authenticated principal. Change regex to meet needs
setSubjectDnRegex("CN=(.*?),.*") }
)
}
// Define an authentication manager that can be used to match an authenticated principal to a user
.authenticationManager {
// Look the user up in the provided userDetailsService to get their authorities/roles
userDetailsService
.findByUsername(it.principal.toString())
.map { user -> UsernamePasswordAuthenticationToken(user, user.password, user.authorities) }
}
// Configure which requests require authentication
.authorizeExchange { requests ->
requests
// Any context path matching /** pattern does not need to be authenticated
.pathMatchers("/public/**").permitAll()
// Any requests to context paths beginning with /admin will need the "ADMIN" role
.pathMatchers("/admin/**").hasRole("ADMIN")
// All other requests need to be authenticated
.anyExchange().authenticated()
}
.build()
}
/*
In memory database of users. The username is extracted from the certificate and
can be matched against this UserDetailsService to add roles for authorization
purposes only. It is only needed if you are not going to also be using authorization
where a specific user must have certain roles to access specific resources
*/
@Suppress("DEPRECATION")
@Bean
fun userDetailsService(): ReactiveUserDetailsService {
val user: UserDetails =
User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
// Create an admin user
val adminUser: UserDetails =
User.withDefaultPasswordEncoder()
.username("admin")
.password("password")
.roles("ADMIN")
.build()
return MapReactiveUserDetailsService(user, adminUser)
}
}
KotlinWebMvc
@SpringBootApplication
@EnableWebSecurity
class WebMvcX509Application {
// Need to define a SecurityFilterChain so we can configure the security we need
@Bean
@Throws(Exception::class)
fun securityFilterChain(
http: HttpSecurity,
userDetailsService: UserDetailsService,
): SecurityFilterChain {
http
// Enable redirect to https protocol
.requiresChannel { it.anyRequest().requiresSecure() }
// Enable SSL/TLS Client authentication using x509 certificates
.x509 {
// Ability to extract the user details from the authenticated certificate's common name (CN)
it.x509PrincipalExtractor(SubjectDnX509PrincipalExtractor().apply {
//Extract the common name (CN) from the certificate. Used to create a authenticated principal. Change regex to meet needs
setSubjectDnRegex("CN=(.*?),.*") }
)
}
// Define an authentication manager that can be used to match an authenticated principal to a user
.authenticationManager {
// Look the user up in the provided userDetailsService to get their authorities/roles
userDetailsService
.loadUserByUsername(it.principal.toString())
.let { user -> UsernamePasswordAuthenticationToken(user, user.password, user.authorities) }
}
// Configure which requests require authentication
.authorizeHttpRequests { requests ->
requests
// Any context path matching /** pattern does not need to be authenticated
.requestMatchers("/public/**").permitAll()
// Any requests to context paths beginning with /admin will need the "ADMIN" role
.requestMatchers("/admin/**").hasRole("ADMIN")
// All other requests need to be authenticated
.anyRequest().authenticated()
}
return http.build()
}
/*
In memory database of users. The username is extracted from the certificate and
can be matched against this UserDetailsService to add roles for authorization
purposes only. It is only needed if you are not going to also be using authorization
where a specific user must have certain roles to access specific resources
*/
@Suppress("DEPRECATION")
@Bean
fun userDetailsService(): UserDetailsService {
//Create a general user
val user: UserDetails =
User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
// Create an admin user
val adminUser: UserDetails =
User.withDefaultPasswordEncoder()
.username("admin")
.password("password")
.roles("ADMIN")
.build()
return InMemoryUserDetailsManager(user, adminUser)
}
}
KotlinConclusion
This article has hopefully helped you to configure mTLS Client Authentication with Spring Boot.
- What configuration you need
- Code examples for the Server side using
- WebFlux
- WebMvc
- Code examples for configuring the client using
- WebFlux
- WebMvc
- How to get finer grained control using the Subject within the certificate and matching those to a user database