x.509 Mutual Authentication

Learn how to implement mutual authentication using SSL and X.509 certificates.

Authors: Burt Beckwith

Micronaut Version: 4.3.7

1. Getting Started

In this guide, we will create a Micronaut application with X.509 authentication written in Kotlin.

2. What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE

  • JDK 1.8 or greater installed with JAVA_HOME configured appropriately

  • OpenSSL installed

3. Solution

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

4. X.509 Mutual Authentication

As of Micronaut Security version 3.3, applications can use X.509 client certificates when using SSL to enable mutual authentication, i.e., in addition to the client verifying the server identity with its SSL certificate, the server can verify the client identity with a client certificate.

5. Create certificates for testing

First, we’ll need a Certificate Authority (CA), an SSL certificate for the server, and an X.509 certificate for each application user who will be using mutual authentication. You can use an existing CA, server certificate, and/or client certificate if you already have them; otherwise create your own as described below.

5.1. Create a Certificate Authority (CA)

Start by creating a self-signed CA with OpenSSL:

openssl req -x509 -sha256 -days 365 -newkey rsa:4096 \
        -keyout cacert.key -out cacert.pem

You will be asked for a CA password ("PEM pass phrase") and a few values for the CA. Most are optional and are useful for identifying a particular certificate; for our needs only the Common Name (CN) is important though - enter a domain name or other identifying value, e.g., "micronaut.guide.x509":

certs1

This will create two files, cacert.pem (the CA in PEM format) and cacert.key (the private key).

You can verify and inspect the CA with

openssl x509 -in cacert.pem -text
openssl x509 -purpose -in cacert.pem -inform PEM

5.2. Create a Server SSL Certificate

Next we’ll create an SSL certificate for the server. This involves creating a certificate signing request (CSR) and creating the certificate by signing the CSR with the CA we created.

5.2.1. Create a Certificate Signing Request (CSR)

openssl req -newkey rsa:4096 -keyout server.key -out server.csr

You will be asked for a password ("PEM pass phrase") and a few values for the certificate. Enter "localhost" for the Common Name (CN); the rest are optional:

certs2

This will create two files, server.csr (the CSR) and server.key (the private key).

You can verify and inspect the CSR with

openssl req -noout -text -in server.csr

5.2.2. Sign the Server CSR with the CA

Next, sign the CSR with the CA to create the server certificate. Create a configuration file server.conf with this content:

server.conf
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost

and generate the certificate:

openssl x509 -req -CA cacert.pem -CAkey cacert.key \
        -in server.csr -out server.pem -days 365 \
        -CAcreateserial -extfile server.conf

You will be asked for the CA password that you chose earlier. Everything else is provided via the command line or server.conf:

certs3

This will create server.pem (the server certificate).

You can verify and inspect the certificate with

openssl x509 -in server.pem -text
openssl x509 -purpose -in server.pem -inform PEM

5.3. Create a Keystore Containing the Server Certificate

5.3.1. Create server.p12

Bundle the server certificate and private key in PKCS #12 format:

openssl pkcs12 -export -out server.p12 -name "localhost" \
        -inkey server.key -in server.pem

You will be asked for the server certificate password that you chose earlier and a new password ("Export Password") for the PKCS #12 archive:

certs4

This will create server.p12. You can verify and inspect the archive with

openssl pkcs12 -in server.p12 -noout -info

5.3.2. Create keystore.p12

Create keystore.p12 containing server.p12 using the keytool utility from your JDK:

keytool -importkeystore -srckeystore server.p12 \
        -srcstoretype pkcs12 -destkeystore keystore.p12 \
        -deststoretype pkcs12

You will be asked for the server.p12 password that you chose earlier and a new password for the keystore:

certs5

This will create keystore.p12. You can verify and inspect the archive with

openssl pkcs12 -in keystore.p12 -noout -info

5.4. Create a Truststore Containing the CA

Create truststore.jks containing the CA:

keytool -import -trustcacerts -noprompt -alias ca \
        -ext san=dns:localhost,ip:127.0.0.1 \
        -file cacert.pem -keystore truststore.jks

You will be asked for a new password ("keystore password") for the truststore:

certs6

5.5. Create a Client X.509 Certificate

5.5.1. Create a Certificate Signing Request (CSR)

Similar to what we did earlier for the server certificate, create a CSR for the client certificate:

openssl req -newkey rsa:4096 -nodes -keyout client.key -out client.csr

For this certificate we’ll skip the password (via the -nodes command line flag), but you can omit the flag and enter a password if you prefer.

Enter a username (e.g., "myusername") for the Common Name (CN); the rest are optional:

certs7

This will create two files, client.csr (the CSR) and client.key (the private key).

You can verify and inspect the CSR with

openssl req -noout -text -in client.csr

5.5.2. Sign the Client CSR with the CA

Sign the client CSR with the CA:

openssl x509 -req -CA cacert.pem -CAkey cacert.key \
        -in client.csr -out client.pem -days 365 -CAcreateserial

You will be asked for the CA password that you chose earlier.

certs8

This will create client.pem (the client certificate).

You can verify and inspect the client certificate with

openssl x509 -in client.pem -text

5.5.3. Create client.p12

Bundle the client certificate and private key in PKCS #12 format:

openssl pkcs12 -export -out client.p12 -name "client" \
        -inkey client.key -in client.pem

You will be asked for a new password for the PKCS #12 archive:

certs9

This will create client.p12. You can verify and inspect the archive with

openssl pkcs12 -in client.p12 -noout -info

6. Writing the Application

Create an application using the Micronaut Command Line Interface or with Micronaut Launch.

mn create-app example.micronaut.micronautguide \
   --features=yaml,security,graalvm,reactor \
   --build=maven --lang=kotlin
If you don’t specify the --build argument, Gradle is used as the build tool.
If you don’t specify the --lang argument, Java is used as the language.
If you don’t specify the --test argument, JUnit is used for Java and Kotlin, and Spock is used for Groovy.

The previous command creates a Micronaut application with the default package example.micronaut in a directory named micronautguide.

If you have an existing Micronaut application and want to add the functionality described here, you can view the dependency and configuration changes from the specified features and apply those changes to your application.

6.1. Copy Certificates

Create the src/main/resources/ssl directory and copy the keystore (keystore.p12) and truststore (truststore.jks) that you created earlier into it.

6.2. Configuration

Replace the generated application.yml with this:

src/main/resources/application.yml
micronaut:
  application:
    name: micronautguide

  security:
    x509:
      enabled: true (1)

  ssl:
    enabled: true (2)

  server:
    ssl:
      client-authentication: want (3)
      key-store:
        path: classpath:ssl/keystore.p12 (4)
        password: (5)
        type: PKCS12 (6)
      trust-store:
        path: classpath:ssl/truststore.jks (7)
        password: (8)
        type: JKS (9)
1 Enable X.509 support
2 Enable SSL/TLS at the server
3 Set client-authentication to want, i.e., accept a client certificate if available, but don’t block access without one. To require a client certificate for all requests, change to need
4 Specify the location of the keystore
5 Set this value with the password you used when creating the keystore
6 Specify the keystore type as PKCS #12
7 Specify the location of the truststore
8 Set this value with the password you used when creating the truststore
9 Specify the truststore type as JKS (Java Key Store)

6.3. HelloController

Create HelloController to test using client X.509 certificates:

src/main/kotlin/example/micronaut/HelloController.kt
package example.micronaut

import io.micronaut.http.MediaType.TEXT_PLAIN
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.security.annotation.Secured
import io.micronaut.security.authentication.Authentication
import io.micronaut.security.rules.SecurityRule.IS_ANONYMOUS
import io.micronaut.security.x509.X509Authentication

@Controller
class HelloController {

    @Secured(IS_ANONYMOUS) (1)
    @Get(produces = [TEXT_PLAIN]) (2)
    fun hello(x509Authentication: X509Authentication?,  (3)
              authentication: Authentication?): String { (4)
        if (x509Authentication == null && authentication == null) {
            return "Hello unknown!" (5)
        }
        if (x509Authentication == null) {
            return "ERROR: Authentication is present but not X509Authentication" (6)
        }
        return if (x509Authentication !== authentication) {
            "ERROR: Authentication and X509Authentication should be the same instance" (7)
        } else "Hello ${x509Authentication.name} (X.509 cert issued by ${x509Authentication.certificate.issuerX500Principal.name})" (8)

    }
}
1 Allow authenticated and non-authenticated requests
2 By default, a Micronaut response uses application/json as Content-Type. We are returning a String, not a JSON object, so we set it to text/plain.
3 Bind the X509Authentication if present, but allow null when not authenticated
4 Redundantly bind the Authentication to verify that it’s the same instance as the X509Authentication
5 Default the username for non-authenticated requests
6 Sanity check that there’s no Authentication if there’s no X509Authentication
7 Sanity check that the Authentication is the X509Authentication
8 Build the response as the username plus info about the certificate CA for the authenticated user

6.4. Writing Tests

Create a test to verify that X.509 authentication works:

src/test/kotlin/example/micronaut/X509Test.kt
package example.micronaut

import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import jakarta.inject.Inject
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

@MicronautTest (1)
class X509Test {

    @Inject
    @field:Client("/")
    lateinit var httpClient: HttpClient (2)

    @Test
    fun testClientCert() {
        val response = httpClient.toBlocking().retrieve("/") (3)
        val expected = "Hello myusername (X.509 cert issued by CN=micronaut.guide.x509)"
        assertEquals(expected, response) (4)
    }
}
1 Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. More info.
2 Inject the HttpClient bean and point it to the embedded server.
3 Make a request to the controller
4 Verify the response is correct

Create application-test.yml in src/test/resources with this content:

src/test/resources/application-test.yml
micronaut:
  ssl:
    port: -1 (1)
  http:
    client:
      ssl:
        key-store:
          path: classpath:ssl/client.p12 (2)
          password: secret (3)
          type: PKCS12 (4)
        insecureTrustAllCertificates: true (5)
1 Start the server on a random unused SSL port
2 Specify the location of the client keystore
3 Change this value to the password you used when creating the client keystore
4 Specify the keystore type as PKCS #12

7. Testing the Application

To run the tests:

./mvnw test

8. Running the Application

To run the application, use the ./mvnw mn:run command, which starts the application on port 8443.

8.1. Testing with cURL

You can run some cURL requests to test the application. First, make a request without presenting a client certificate:

curl -i --cacert cacert.pem https://localhost:8443/

The output should look similar to this:

HTTP/1.1 200 OK
date: Thu, 28 Oct 2021 01:03:40 GMT
Content-Type: text/plain
content-length: 14
connection: keep-alive

Hello unknown!

Next, make a request with a client certificate:

curl -i --cert client.pem --key client.key \
     --cacert cacert.pem https://localhost:8443/

The output should look similar to this:

HTTP/1.1 200 OK
date: Thu, 28 Oct 2021 01:05:25 GMT
Content-Type: text/plain
content-length: 63
connection: keep-alive

Hello myusername (X.509 cert issued by CN=micronaut.guide.x509)

8.2. Testing with a Web Browser

To make requests in a web browser, you must import the CA and the client certificate into the browser.

8.2.1. Import Certificates Into Firefox

To do this in Firefox, navigate to about:preferences#privacy and scroll down to the Certificates section, then click "View Certificates":

browser1

Go to the "Authorities" tab and click "Import…​":

browser2

Navigate to the cacert.pem CA certificate you created earlier and import it. Check the "Trust this CA to identify websites" checkbox and click OK:

browser3

The CA will appear as a "Software Security Device" under "micronaut.guide.x509":

browser4

Go to the "Your Certificates" tab and click "Import…​":

browser5

Navigate to the client.p12 archive you created earlier and import it. The certificate will appear as a "Software Security Device" under "myusername":

browser6

8.2.2. Import Certificates Into Chrome

To do this in Chrome, navigate to chrome://settings/certificates and go to the "Authorities" tab, then click "Import":

browser7

Navigate to the cacert.pem CA certificate you created earlier and import it. Check the "Trust this certificate for identifying websites" checkbox and click OK:

browser8

The CA will appear under "org-micronaut.guide.x509":

browser9

Go to the "Your certificates" tab and click "Import…​":

browser10

Navigate to the client.p12 archive you created earlier and import it. The certificate will appear under "org-myusername":

browser11

8.2.3. Make a secure request

Navigate to https://localhost:8443 and choose the client certificate you imported earlier when prompted. The response should look like this:

browser12

9. Generate a Micronaut Application Native Executable with GraalVM

We will use GraalVM, the polyglot embeddable virtual machine, to generate a native executable of our Micronaut application.

Compiling native executables ahead of time with GraalVM improves startup time and reduces the memory footprint of JVM-based applications.

Only Java and Kotlin projects support using GraalVM’s native-image tool. Groovy relies heavily on reflection, which is only partially supported by GraalVM.

9.1. GraalVM installation

The easiest way to install GraalVM on Linux or Mac is to use SDKMan.io.

Java 17
sdk install java 17.0.8-graal
Java 17
sdk use java 17.0.8-graal

For installation on Windows, or for manual installation on Linux or Mac, see the GraalVM Getting Started documentation.

The previous command installs Oracle GraalVM, which is free to use in production and free to redistribute, at no cost, under the GraalVM Free Terms and Conditions.

Alternatively, you can use the GraalVM Community Edition:

Java 17
sdk install java 17.0.8-graalce
Java 17
sdk use java 17.0.8-graalce

9.2. Native executable generation

To generate a native executable using Maven, run:

./mvnw package -Dpackaging=native-image

The native executable is created in the target directory and can be run with target/micronautguide.

Run the native executable and repeat the cURL calls and browser request that you made earlier.

10. Next steps

Explore more features with Micronaut Guides.

Also check out Micronaut Acme, a convenient way to configure and refresh SSL certificates using Automatic Certificate Management Environment (ACME)

11. License

All guides are released with an Apache license 2.0 license for the code and a Creative Commons Attribution 4.0 license for the writing and media (images…​).