Spring Boot: Authentication & Authorization with SSL Certificate

Spring Boot: Authentication & Authorization with SSL Certificate

ALBEDO Le
ALBEDO Le

Introduction

HTTPS is an extension of HTTP that allows secure communications between two entities in a computer network. HTTPS uses the TLS (Transport Layer Security) protocol to achieve secure connections.

TLS can be implemented with one-way or two-way certificate verification. In the one-way, the server shares its public certificate so the client can verify that it's a trusted server. The alternative is two-way verification. Both the client and the server share their public certificates to verify each other's identity.

This post will focus on two-way certificate verification, where the server will also check the client's certificate.

Generating Certificates

Since we're doing a two-way TLS authentication, we'll need to generate certificates for the client and the server.

In a production environment, it's recommended to purchase the certificates from a Certificate Authority. However, for testing or demo purposes, it's good enough to use self-signed certificates. For this post, we're going to use Java's keytool to generate the self-signed certificates.

1. Client Certificate

First, we generate the Admin key store:

keytool -genkey -alias adminkey -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -keystore adminkeystore.p12 -storepass password -ext san=ip:127.0.0.1,dns:localhost

We use the keytool -ext option to set the Subject Alternative Names (SAN) to define the local hostname/IP address that identifies the server. In this example, We will use CN (Common Name) for authorization in our application. Set common name is "Admin"

Next, we export the certificate to the file admin-certificate.pem:

keytool -exportcert -keystore adminkeystore.p12 -alias adminkey -storepass password -rfc -file admin-certificate.pem

Finally, we add the admin certificate to the server's trust store:

keytool -import -trustcacerts -alias adminkey -file admin-certificate.pem -keypass password -storepass password -keystore servertruststore.jks

Similarly, we generate the user key (CN=User) store and export its certificate

keytool -genkey -alias userkey -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -keystore userkeystore.p12 -storepass password -ext san=ip:127.0.0.1,dns:localhost

keytool -exportcert -keystore userkeystore.p12 -alias userkey -storepass password -rfc -file user-certificate.pem

keytool -import -trustcacerts -alias userkey -file user-certificate.pem -keypass password -storepass password -keystore servertruststore.jks

2. Server Certificate

We generate the server key store:

keytool -genkey -alias serverkey -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -keystore serverkeystore.p12 -storepass password -ext san=ip:127.0.0.1,dns:localhost

In this server side, the server only needs to have a keystore and truststore (containing the client's key) to be able to authenticate client requests. If we need to communicate between two microservices (A calls B, and B also calls A), then we just need to import the server's key into the client's truststore.

Server Java Implementation

All required dependencies are shown here:

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

Configure the Server to Serve HTTPS Content

# The path to the keystore containing the certificate
server.ssl.key-store=classpath:serverkeystore.p12
# The password used to generate the certificate
server.ssl.key-store-password=password

Configure the Server to Require a Client Certificate

# Trust store that holds SSL certificates.
server.ssl.trust-store=classpath:servertruststore.jks
# Password used to access the trust store.
server.ssl.trust-store-password=password
# Whether client authentication is wanted ("want") or needed ("need").
server.ssl.client-auth=need

The embedded server now ensures (without any other configuration) that the clients with a valid certificate are only able to call our REST API.

Other clients will be declined by the server due to being unable to make the correct SSL/TLS handshake (required by mutual authentication).

Let's create a simple REST controller with an Admin role and a User role

@RestController
public class Controller {

    @GetMapping("admin")
    @Secured("ROLE_ADMIN")
    public String apiAdmin() {
        return "ADMIN hereeeee!";
    }

    @GetMapping("user")
    @Secured({"ROLE_ADMIN", "ROLE_USER"})
    public String apiUser() {
        return "This is a User";
    }
}

Until now, all clients with any valid certificate may perform any call in our application without knowing who the caller is.

So we must configure Spring Security to create a logged user using a username from a client certificate (usually from the CN field; see the method call subjectPrincipalRegex):

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .x509()
                .subjectPrincipalRegex("CN=(.*?)(?:,|$)") //CN means Common Name
                .userDetailsService(userDetailsService());
    }

    @Bean
    public UserDetailsService userDetailsService() { //Can use to validation user and roles
        return username -> {
            if (username.equals("Admin")) {
                return new User(username, "", AuthorityUtils.createAuthorityList("ROLE_ADMIN", "ROLE_USER"));
            } else if (username.equals("User")) {
                return new User(username, "", AuthorityUtils.createAuthorityList("ROLE_USER"));
            } else {
                //403 status code
                throw new UsernameNotFoundException(String.format("User %s not found", username));
            }
        };
    }
}

Using the bean UserDetailsService is a kind of fake, but it shows an example of an additional authentication to accept only the username "Admin" and "User".

In other words, it accepts a client with a certificate containing the value "Admin" or "User" only in the certificate's CN field (as mentioned before, configured with subjectPrincipalRegex).

Test Secured REST API

1. Test with Browser

Initially, when you enter https://localhost:8080/ or https://localhost:8080/user in your browser, you will get an ERR_BAD_SSL_CLIENT_AUTH_CERT error because you don't have a valid certificate

Don't worry, you just need to install "Admin" and "User" certificates on your machine, you will be connected immediately

When you reload the page, the page will ask you which certificate to use, you just need to select the certificate

Some test cases:
The USER enter https://localhost:8080/user

The USER enter https://localhost:8080/admin

The ADMIN enter https://localhost:8080/user

The ADMIN enter https://localhost:8080/admin

2. Test with Postman

To be able to test with Postman, you need to import the client's certificate.
You can go to File -> Settings -> Certificates -> Client Certificates -> Add Certificates