openssl req -x509 -sha256 -days 365 -newkey rsa:4096 \
-keyout cacert.key -out cacert.pem
x.509 Mutual Authentication
Learn how to implement mutual authentication using SSL and X.509 certificates.
Authors: Burt Beckwith
Micronaut Version: 3.9.2
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.
-
Download and unzip the source
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:
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":
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:
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:
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
:
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:
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:
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:
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:
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.
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:
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=security,graalvm,reactor \
--build=gradle --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.
|
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:
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:
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:
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:
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:
./gradlew test
Then open build/reports/tests/test/index.html
in a browser to see the results.
8. Running the Application
To run the application, use the ./gradlew 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":
Go to the "Authorities" tab and click "Import…":
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:
The CA will appear as a "Software Security Device" under "micronaut.guide.x509":
Go to the "Your Certificates" tab and click "Import…":
Navigate to the client.p12
archive you created earlier and import it. The certificate will appear as a "Software Security Device" under "myusername":
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":
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:
The CA will appear under "org-micronaut.guide.x509":
Go to the "Your certificates" tab and click "Import…":
Navigate to the client.p12
archive you created earlier and import it. The certificate will appear under "org-myusername":
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:
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. Native executable generation
The easiest way to install GraalVM on Linux or Mac is to use SDKMan.io.
sdk install java 22.3.r11-grl
If you still use Java 8, use the JDK11 version of GraalVM. |
sdk install java 22.3.r17-grl
For installation on Windows, or for manual installation on Linux or Mac, see the GraalVM Getting Started documentation.
After installing GraalVM, install the native-image
component, which is not installed by default:
gu install native-image
To generate a native executable using Gradle, run:
./gradlew nativeCompile
The native executable is created in build/native/nativeCompile
directory and can be run with build/native/nativeCompile/micronautguide
.
It is possible to customize the name of the native executable or pass additional parameters to GraalVM:
graalvmNative {
binaries {
main {
imageName.set('mn-graalvm-application') (1)
buildArgs.add('--verbose') (2)
}
}
}
1 | The native executable name will now be mn-graalvm-application |
2 | It is possible to pass extra arguments to build the native executable |
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)