Build a Microprofile Rest Client with Mutual TLS Authentication
In networking, layers are used to understand the complexity of networks. Some network models such as the OSI and TCP/IP have been key in modern-day networks. Security in systems is important considering that Security is not an afterthought of a system, rather an integral part. <!--more--> In Microprofile, security is key. It is built side by side as the system is being developed to prevent unwanted integration errors.
One way of improving security is by adding TLS authentication to the application.
In this article, you will learn how to build a Microprofile Rest Client application with TLS authentication.
Table of contents
- Key takeaways
- Prerequisites
- What is TLS authentication?
- Getting started
- Modify the Client and Server endpoints
- Add Server configurations
- Add Client configurations
- Add a Client interface and import configuration properties
- Access the Server from Client using a simple Client call
- Generate the KeyStores and TrustStores
- Run the application
- Make a Server call using a ClientBuilder
- Conclusion
- Further reading
- References
Key takeaways
At the end of this article, you will have gained the following knowledge:
- What TLS authentication is
- How and where it can be implemented
- Setting up Mutual TLS authentication in a Microprofile application such as Quarkus
- Running the application
Prerequisites
The basics of the article include the following:
- Java language knowledge and use. It will be easy for intermediate Java developers.
- Java installed and set up on the machine. For the article, you need version
11+
. - A Java IDE is set up in your machine. I recommend using the latest IntelliJ ultimate edition. It is very interactive, and it has all the necessary tools to use for Java frameworks.
- A stable internet connection.
NOTE: The screenshots taken for this article are from the IntelliJ Ultimate Edition version
2021.2.2
. The Java version used is Java Version 17.
What is TLS authentication?
TLS (Transport Layer Security) is a cryptographic protocol utilizing both Symmetric and Asymmetric cryptography to secure message passing from one machine to another.
It prevents unauthorized people with malicious intends from eavesdropping when the message is passed from the server to the client. It only protects the transportation of the messages but not end device security.
This is because it is implemented at the top of the TCP/IP layer to encrypt Application Layer protocols e.g. HTTP, FTP, IMAP, and SMTP, though it can be implemented also on UDP, DCCP, and SCTP as well (e.g. for VPN and SIP-based application uses).
This implementation is known as DTLS (Datagram Transport Layer Security) and is specified in RFCs 6347, 5238, and 6083.
TLS is an evolved SSL (Secure Socket Layers). It allows the server and client to exchange keys that are used to encrypt the messages.
TLS can be used in websites to enforce security on email services, banking sites, ecommerce sites, among many other sites.
Once enabled, one notices a padlock icon at the left-hand side of the URL in the browser. This shows that the site is secure.
It looks as shown below:
When the padlock icon is clicked, the following pop up is shown:
Always avoid visiting sites without the SSL padlock icon which shows that they are insecure.
Getting started
Initialize a new maven project. Set the name and the artifactId to quarkus-restclient-mutual-tls
.
Click on finish.
In the project generated, delete the src file.
This will reduce confusion so that the focus can be on the new modules to be created.
In it, create a new module by navigating to the File > New > Module
option as shown below:
In the newly opened window, select the project type to be Quarkus.
The following will be its configurations:
Name
: quarkus-serverArtifactId
: quarkus-serverGroup
: org.gs
As for the dependencies, select the RESTEasy JAX-RS
and RESTEasy Jackson
as shown below:
Create a second module and this time set the following:
Name
: quarkus-clientArtifactId
: quarkus-clientGroup
: org.gs
Choose the RESTEasy JAX-RS
, RESTClient
, and RESTClient Jackson
as the dependencies, as shown below:
The project structure is as shown below:
Delete the contents in the test folder, both in the quarkus-client
and quarkus-server
modules.
Deletion allows a quick run without the additional unit tests.
Right-click on the quarkus-client
module and select Open in > Terminal
option. This opens it in the IntelliJ integrated terminal.
Do the same to the quarkus-server
module so that they can be opened in two different terminals.
Run them to check if all dependencies are well installed or for any errors by running the following:
- On
quarkus-client
terminal, run./mvnw quarkus:dev
. Open a new terminal and test the Client by runningcurl http://localhost:8080/hello
to get a response from the Client. If the response is "Hello RESTEasy", it works correctly. Stop it to run the server on the other terminal. - On
quarkus-server
terminal, run./mvnw quarkus:dev -Ddebug=5006
. This changes the debugging port from the default 5005 to 5006 to avoid port conflicts. Runcurl http://localhost:8080/hello
for server response. Stop it.
Modify the client and server endpoints
Head over to the ExampleResource.java
file inside the quarkus-client
module:
- Change the class in it and the name of the file to
ClientResource
. - Change the output to "Hello from Client" by changing the return of the GET request to
Hello from Client\n
. - Change the end-point to
/client
.
In the ExampleResource.java
file inside the quarkus-server
module, do the following:
- Change the class in it and the name of the file to
ServerResource
. - Change the output to "Hello from Client" by changing the return of the GET request to
Hello from Server\n
. - Change the end-point to
/server
.
Rerun the modules separately as done before using the set endpoints to make sure the return is as desired.
Add server configurations
In the Server, open the application.properties
file. This file is located under the quarkus-server/src/main/resources
path.
In the file, do the following:
- Disable http requests and enable using https requests. Https allows for the application to use secure SSL. To disable http, use port number 0.
Use SSL at port number 8443 as in the code below:
# Enable ssl but disable http to enforce security
quarkus.http.ssl-port=8443
quarkus.http.port=0
- Set the application to require SSL client authentication. This will require the SSL keys during the connection. Check it out below:
# Set the server to require ssl authentication
quarkus.http.ssl.client-auth=required
A KeyStore will be generated in the server which will be used as a client's TrustStore. It will be used during server authentication.
The same will be done during client authentication. A client KeyStore will be stored in the server as a TrustStore.
This is shown as in the diagram below:
TrustStore is used to store Certified Authorities (CA) certificates verifying the certificate presented by the server in an SSL connection.<br/>The Keystore on the other hand is used to store private key and identity certificates presented to both parties (server or client) by a program for verification. Read more here.
Both the KeyStore and TrustStore generated have passwords. These correct passwords are required during the process.
- Add the location of the KeyStore and TrustStore together with their passwords. The code is shown below:
# Set the location of the server keystore and it's password
quarkus.http.ssl.certificate.key-store-file=META-INF/resources/server.keystore
quarkus.http.ssl.certificate.key-store-password=server_password
# Set the location of the server truststore and it's password
quarkus.http.ssl.certificate.trust-store-file=META-INF/resources/client.truststore
quarkus.http.ssl.certificate.trust-store-password=client_password
- Rerun the Server in another window and try accessing the server endpoint using
curl https://localhost:8443/server
. This produces an error immediately since there are no certificates provided. - Try accessing it further with
curl -k https://localhost:8443/server
which quickly checks for wrong configurations or other fails. That produces an error too as shown below:
Add Client configurations
In the Client, open the application.properties
file. This file is located under the quarkus-client/src/main/resources
path.
In the file, do the following:
- Use the
MicroProfile Rest Client (mp-rest)
for invocation of RESTful services over HTTP. The good thing is that it is a type-safe approach. In it, point out to the server url, the trustStore, and the trustStorePassword, and the Keystore together with its password as the Keystore password.
Check it below:
# Use the mp-rest to pass the keystore and truststore from client to server and back
# Set the url, TrustStore and TrustStore password, KeyStore and KeyStore password to be used during the server call process
org.gs.Client/mp-rest/url=https://localhost:8443
org.gs.Client/mp-rest/trustStore=classpath:/META-INF/resources/server.truststore
org.gs.Client/mp-rest/trustStorePassword=server_password
org.gs.Client/mp-rest/keyStore=classpath:/META-INF/resources/client.keystore
org.gs.Client/mp-rest/keyStorePassword=client_password
- Add other configuration properties that can be easily injected into the module for injection in the application and also reuse.
Check its code below:
# Add some config properties
url=https://localhost:8443
keyStore=META-INF/resources/client.keystore
keyStorePassword=client_password
trustStore=META-INF/resources/server.truststore
trustStorePassword=server_password
Add a client interface and import configuration properties
Inside the client, at the location of the ClientResource.java
file, add a new file named 'Client.java'. This will be an interface used to access the server API. When all conditions given are met, the server shall be called.
The code for the interface is as shown below:
package org.gs;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
@RegisterRestClient
@Path("/")
public interface Client {
// Access the server when all conditions required are met
@GET
@Path("/server")
@Produces(MediaType.TEXT_PLAIN)
String call();
}
Access the server from client using a simple Client call
- First, inside the 'ClientResource' class, just above the 'hello()' string, import the application configurations. Import the server url for the call, KeyStore and its password, and the TrustStore and its password.
See it in the code below:
/*
Inject configuration properties to the app; they are stored in the application.properties file. These shall be used during the connection between the server and the client
These include: - URL to be used to contact the server
- The KeyStore and Keystore password
- The truststore and truststore password
*/
@ConfigProperty(name = "url")
URL serverURL;
@ConfigProperty(name = "keyStore")
String keyStoreFile;
@ConfigProperty(name = "keyStorePassword")
String keyStoreFilePassword;
@ConfigProperty(name = "trustStore")
String trustStoreFile;
@ConfigProperty(name = "trustStorePassword")
String trustStoreFilePassword;
- Add a
RestClient
annotation to show that it is a rest client. Inject the interface initially generated.
/*
* Annotate that the application is a REST_CLIENT
* Inject the client interface into the application. This will be used as an interface between the server and client
*/
@RestClient
@Inject
Client client;
- Add an endpoint used to access the server. The endpoint will be accessed via
http://localhost:8080/client/client
.
/*
* Create a simple GET request that will use the configurations entered to contact the server
*/
@GET
@Path("client")
@Produces(MediaType.TEXT_PLAIN)
public String callWithClient() {
return client.call();
}
It uses the mp-rest
configuration set initially in the client's application.properties
file.
Generate the KeyStores and TrustStores
Inside the Client, in the 'resources/META-INF/resources' folder create a new file with the name 'generate_client_keystore.sh'. As seen, the file holds shell scripts that can be run.
Open the file and copy and paste the code below into it:
keytool -genkeypair \
-storepass client_password \
-keyalg RSA \
-keysize 2048 \
-dname "CN=client" \
-alias client \
-ext "SAN:c=DNS:localhost,IP:127.0.0.1" \
-keystore client.keystore \
&&
cp client.keystore \
../../../../../../quarkus-server/src/main/resources/META-INF/resources/client.truststore
When run, the code will do the following:
- Generates public and associated private keys
- The password for the key is
client_password
. Remember that this password was set in the server'sapplication.property
file as the client's TrustStore. - The underlying key algorithm to
RSA
. - The size of the key to 2 MB (
2048
). - A distinguished name of
CN=client
. - An Alias name of the entry to process to
client
. - An X.509 extension as follows:
SAN:c=DNS:localhost,IP:127.0.0.1
. Read more of the X.509 extension here or here. - The Keystore file name to
client.keystore
- Once generated, the application will copy it into the server's resource file as
client.truststore
Run it by navigating to it and pressing down the combination Ctrl + Shift + F10
or the Run button as seen in IntelliJ. This is shown below:
Create a new file inside the server module, under 'resources/META-INF/resources' file, create a file and name it 'generate_server_keystore.sh'.
Copy the code below and paste it into the file:
keytool -genkeypair \
-storepass server_password \
-keyalg RSA \
-keysize 2048 \
-dname "CN=server" \
-alias server \
-ext "SAN:c=DNS:localhost,IP:127.0.0.1" \
-keystore server.keystore \
&& \
cp server.keystore \
../../../../../../quarkus-client/src/main/resources/META-INF/resources/server.truststore
It does as the previous file except with a few modifications to support the server side.
The files created should look as shown below:
.
├── quarkus-client
│ └── src
│ └── main
│ ├── docker
│ ├── java
│ └── resources
│ └── META-INF
│ ├── resources
│ ├── client.keystore
│ ├── generate_server_keystore.sh
│ └── server.truststore
│ └── application.properties
├── quarkus-server
│ └── src
│ └── main
│ ├── docker
│ ├── java
│ └── resources
│ └── META-INF
│ ├── resources
│ ├── client.truststore
│ ├── generate_server_keystore.sh
│ └── server.keystore
│ └── application.properties
When creating an online repo, e.g. a GitHub repo, don't store the KeyStore or the TrustStore online. When GitGuardian is activated, it immediately shows that this is a dangerous move and can be exploited. Rather, store the generators which can be used to generate others securely.
Run the application
To run the application to make sure that the client and server have exchanged the files and used the passwords to verify them:
- Open both modules in different internal terminals.
- On the client's terminal, run
./mvnw quarkus:dev
. - On the server's terminal, run
./mvnw quarkus:dev -Ddebug=5006
. - Access the server via the client by use of
http://localhost:8080/client/client
The response will be Hello from Server
.
Make a server call using a ClientBuilder
In the 'ClientResources.java' file, do the following:
- Add a new endpoint to be used to check if the Builder comes up with the same result as before. Do this by adding the block of code below:
/*
* Create a GET request using a builder that will use the configurations entered to contact the server
*/
@GET
@Path("clientBuilder")
@Produces(MediaType.TEXT_PLAIN)
- Add the
callWithClientBuilder
function and the errors it throws in case of any. This is shown below:
/*
* Create a method to call the server using a Client_Builder
* Some errors it may give include: - KeyStoreException error
- CertificateException error
- NoSuchAlgorithmException error
- IOException error
*/
public String callWithClientBuilder() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException {
}
- In the function above, fetch the KeyStore and its password which was imported as configuration properties before. Check it out below:
/*
* Get the KeyStore file as a stream and store it in a Keystore object for use during the server call
* On load, use the Keystore and Keystore file password
*/
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream inputStreamKeyStore = this.getClass()
.getClassLoader()
.getResourceAsStream(keyStoreFile);
keyStore.load(inputStreamKeyStore, keyStoreFilePassword.toCharArray());
- Fetch the trustStore and its password. Check it out below:
/*
* Get the TrustStore file as a stream and store it in a Keystore object for use during the server call
* On load, use the TrustStore and TrustStore file password
* The TrustStore is of a KeyStore type
*/
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream inputStreamTrustStore = this.getClass()
.getClassLoader()
.getResourceAsStream(trustStoreFile);
trustStore.load(inputStreamTrustStore, trustStoreFilePassword.toCharArray());
- Use the KeyStore objects fetched to build the url up so that the server call can be made:
/*
* Set the url, keyStore, and trustStore during the build
* Make the server call at the end of the build function
*/
Client clientBuild = RestClientBuilder.newBuilder()
.baseUrl(serverURL)
.keyStore(keyStore, keyStoreFilePassword)
.trustStore(trustStore)
.build(Client.class);
return clientBuild.call();
Save the application and rerun it as previously done but now using the http://localhost:8080/client/clientBuilder
.
It gives out the outcome as the initial one, a Hello from Server
reply. This shows that the client has contacted the server properly and securely.
Any challenges in the code? Find the repository containing the code here.
Conclusion
In the article, the following have been accomplished:
- What TLS authentication is.
- Where and how TLS can be implemented.
- Setting up Mutual TLS authentication in a MicroProfile application such as Quarkus.
- Running the application.
Further reading
Increase understanding of TLS on topics such as: How TLS works, Certificate Authority (CA), X.509 standard for Public Key Infrastructures (PKIs), among others here.
References
Peer Review Contributions by: Daniel Katungi