© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2021
N. LamouchiPro Java Microservices with Quarkus and Kuberneteshttps://doi.org/10.1007/978-1-4842-7170-4_6

6. Adding Anti-Disaster Layers

Nebrass Lamouchi1  
(1)
Paris, France
 

Introduction

Writing code, running unit and integration tests, doing code quality analysis, creating CI/CD pipelines—many developers think that the trip ends there, and a new iteration will start again. We forget the application runtime. I don’t mean where it’s to be executed, we already said that we will be running this application in Docker containers. I’m talking about how the application will run:
  • How will users use QuarkuShop?

  • How will user access to the application be controlled?

  • Can we handle unauthorized access? Do we know which ones to admit and which ones to reject?

  • How is the consumption of CPU and memory resources measured and tracked?

  • What will happen if the application runs out of resources?

There are even more questions to be asked about the runtime. These questions reveal two missing layers in QuarkuShop:
  • The Security Layer: All the authentication and authorization parts.

  • The Monitoring Layer: All the metrics, I.e., the measurement and tracking components.

Implementing the Security Layer

Security! One of the most painful topics for developers, ../images/509649_1_En_6_Chapter/509649_1_En_6_Figa_HTML.gif but it’s probably the most critical subject in any enterprise application. Security has been always a very challenging subject in IT: technology and frameworks keep evolving and hackers are evolving more and more. ../images/509649_1_En_6_Chapter/509649_1_En_6_Figb_HTML.gif

For this QuarkuShop, we will use the dedicated Quarkus components and the recommended practices and design choices. This chapter discusses how to implement a typical authentication and authorization engine.

../images/509649_1_En_6_Chapter/509649_1_En_6_Figc_HTML.gif I reference the authentication and authorization process as auth2.

Analyzing Security Requirements and Needs

Before writing any code, we start by creating the design, using UML diagrams for example. The same is true for the security layer; we need to create the design before implementing the code. But which design? The code is there. What will we be designing?

The full QuarkuShop features have been implemented, but there is a lot to be designed.

I like to compare building software to building houses. What we have done up to now is:
  • Built the house, which is the same as writing the source code.

  • Validated the conformity of the building with the plans, which is the same as writing tests.

  • Connected the house to the electricity, water, and sewerage networks, which is the same as configuring access to databases, SonarCloud, etc.

  • Got the furniture and decorated the house, which is the same as creating the CI/CD pipelines.

The house is now ready and the owners want to have a security system. We start by checking the windows and doors to locate possible access points, which is where we place the locks. Only key holders can enter, depending on the person, and the owner will allocate the keys. For example, only the driver will have the car garage key. The gardener will have two keys: one to the external door and one to the garden shed, where the tools are stored. The family members living in the house will have all the keys without exceptions.

We will have also cameras and sensors to monitor and audit access to the house. When we suspect illegal access to the house, we can check the cameras and see what happened.

This home security system deployment process is somehow the same as adding the Security layer of an application. We follow the same basic steps:
  1. 1.

    We analyze and locate all the access points to our application. This process is called attack surface analysis.

     

Attack surface analysis helps you:

  • Identify which functions and parts of the system you need to review/test for security vulnerabilities.

  • Identify high-risk areas of code that require defense-in-depth protection; what parts of the system that you need to defend.

  • Identify when you have changed the attack surface and need to do some kind of threat assessment.

—OWASP Cheat Sheet Series https://cheatsheetseries.owasp.org/cheatsheets/Attack_Surface_Analysis_Cheat_Sheet.html

  1. 2.

    We will put locks on these access points. These locks are part of the authentication process.

     

Authentication is the process of verifying that an individual, entity, or website is whom it claims to be. Authentication in the context of web applications is commonly performed by submitting a username or ID and one or more items of private information that only a given user should know.

—OWASP Cheat Sheet Series https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html

  1. 3.

    We will define an access control mechanism to be sure that only the allowed person can access a given “door”. This process is called authorization.

     

Authorization is the process where requests to access a particular resource should be granted or denied. It should be noted that authorization is not equivalent to authentication - as these terms and their definitions are frequently confused. Authentication is providing and validating identity. The authorization includes the execution rules that determine which functionality and data the user (or Principal) may access, ensuring the proper allocation of access rights after authentication is successful.

—OWASP Cheat Sheet Series https://cheatsheetseries.owasp.org/cheatsheets/Access_Control_Cheat_Sheet.html

QuarkuShop is a Java Enterprise application that exposes REST APIs, which is the only communication channel with the application users.

The users of QuarkuShop can be divided into three categories:
  • Visitor or Anonymous: An unauthenticated customer

  • User: An authenticated customer

  • Admin: The application superuser

The next step is to define which user category is allowed to access each REST API service. This can be done using the authorization matrix.

Defining Authorization Matrices for REST APIs

Authorization Matrix for the Cart REST API

Operation

Anonymous

User

Admin

Get All Carts

../images/509649_1_En_6_Chapter/509649_1_En_6_Figd_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Fige_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figf_HTML.gif

Get Active Carts

../images/509649_1_En_6_Chapter/509649_1_En_6_Figg_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figh_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figi_HTML.gif

Get Carts by Customer ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figj_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figk_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figl_HTML.gif

Create a New Cart for a Given Customer

../images/509649_1_En_6_Chapter/509649_1_En_6_Figm_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Fign_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figo_HTML.gif

Get a Cart by ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figp_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figq_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figr_HTML.gif

Delete a Cart by ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figs_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figt_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figu_HTML.gif

Authorization Matrix for the Category REST API

Operation

Anonymous

User

Admin

List All Categories

../images/509649_1_En_6_Chapter/509649_1_En_6_Figv_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figw_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figx_HTML.gif

Create New Category

../images/509649_1_En_6_Chapter/509649_1_En_6_Figy_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figz_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figaa_HTML.gif

Get Category by ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figab_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figac_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figad_HTML.gif

Delete Category by ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figae_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figaf_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figag_HTML.gif

Get Products by Category ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figah_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figai_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figaj_HTML.gif

Authorization Matrix for the Customer REST API

Operation

Anonymous

User

Admin

Get All Customers

../images/509649_1_En_6_Chapter/509649_1_En_6_Figak_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figal_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figam_HTML.gif

Create a New Customer

../images/509649_1_En_6_Chapter/509649_1_En_6_Figan_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figao_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figap_HTML.gif

Get Active Customers

../images/509649_1_En_6_Chapter/509649_1_En_6_Figaq_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figar_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figas_HTML.gif

Get Inactive Customers

../images/509649_1_En_6_Chapter/509649_1_En_6_Figat_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figau_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figav_HTML.gif

Get Customer by ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figaw_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figax_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figay_HTML.gif

Delete Customer by ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figaz_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figba_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbb_HTML.gif

Authorization Matrix for the Order REST API

Operation

Anonymous

User

Admin

Get All Orders

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbc_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbd_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbe_HTML.gif

Create a New Order

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbf_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbg_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbh_HTML.gif

Get Orders by Customer ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbi_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbj_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbk_HTML.gif

Check if There Is an Order for a Given ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbl_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbm_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbn_HTML.gif

Get Order by ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbo_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbp_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbq_HTML.gif

Delete Order by ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbr_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbs_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbt_HTML.gif

Authorization Matrix for the Order-Item REST API

Operation

Anonymous

User

Admin

Create a New Order-Item

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbu_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbv_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbw_HTML.gif

Get Order-Items by Order ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbx_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figby_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figbz_HTML.gif

Get Order-Item by ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figca_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figcb_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figcc_HTML.gif

Delete Order-Item by ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figcd_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figce_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figcf_HTML.gif

Authorization Matrix for the Payment REST API

Operation

Anonymous

User

Admin

Get All Payments

../images/509649_1_En_6_Chapter/509649_1_En_6_Figcg_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figch_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figci_HTML.gif

Create a New Payment

../images/509649_1_En_6_Chapter/509649_1_En_6_Figcj_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figck_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figcl_HTML.gif

Get Payments for Amounts Inferior or Equal a Limit

../images/509649_1_En_6_Chapter/509649_1_En_6_Figcm_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figcn_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figco_HTML.gif

Get a Payment by ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figcp_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figcq_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figcr_HTML.gif

Delete a Payment by ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figcs_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figct_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figcu_HTML.gif

Authorization Matrix for the Product REST API

Operation

Anonymous

User

Admin

Get All Products

../images/509649_1_En_6_Chapter/509649_1_En_6_Figcv_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figcw_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figcx_HTML.gif

Create a New Product

../images/509649_1_En_6_Chapter/509649_1_En_6_Figcy_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figcz_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figda_HTML.gif

Get Products by Category ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdb_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdc_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdd_HTML.gif

Count All Products

../images/509649_1_En_6_Chapter/509649_1_En_6_Figde_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdf_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdg_HTML.gif

Count Products by Category ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdh_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdi_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdj_HTML.gif

Get a Product by ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdk_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdl_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdm_HTML.gif

Delete a Product by ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdn_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdo_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdp_HTML.gif

Authorization Matrix for the Review REST API

Operation

Anonymous

User

Admin

Get Reviews by Product ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdq_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdr_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figds_HTML.gif

Create a New Review by Product ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdt_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdu_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdv_HTML.gif

Get a Review by ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdw_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdx_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdy_HTML.gif

Delete a Review by ID

../images/509649_1_En_6_Chapter/509649_1_En_6_Figdz_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figea_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figeb_HTML.gif

Implementing the Security Layer

We will use dedicated Identity storage to handle the QuarkuShop users' credentials. We use Keycloak for this purpose.

What is Keycloak?
Keycloak is an open-source identity and access management (IAM) solution aimed at modern applications and services. It makes it easy to secure applications and services with little to no code.
../images/509649_1_En_6_Chapter/509649_1_En_6_Figec_HTML.png

Users authenticate with Keycloak rather than with individual applications. This means that your applications don’t have to deal with login forms, authenticating users, and storing users. Once logged-in to Keycloak, users don’t have to log in again to access a different application. This also applied to logout.

Keycloak provides single-sign out, which means users only have to log out once to be logged out of all applications that use Keycloak.

Keycloak is based on standard protocols and provides support for OpenID Connect, OAuth 2.0, and SAML.

If role-based authorization doesn’t cover your needs, Keycloak provides fine-grained authorization services as well. This allows you to manage permissions for all your services from the Keycloak admin console and gives you the power to define exactly the policies you need.

We will implement this security policy in four steps:
  1. 1.

    Preparing and configuring Keycloak.

     
  2. 2.

    Implementing the auth2 Java components in QuarkuShop.

     
  3. 3.

    Updating the integration tests to support auth2.

     
  4. 4.

    Adding Keycloak to our Production environment.

     

Preparing and Configuring Keycloak

The first step in Security implementation is to have a Keycloak instance. This section discusses creating and configuring Keycloak step-by-step.

../images/509649_1_En_6_Chapter/509649_1_En_6_Figed_HTML.gif In many tutorials, there are prepared configurations that can be imported to Keycloak to get started easily. You will not be doing this here. You will be going step by step to perform all the needed configuration. This is the best way to learn: learning by doing.

Start by creating the Keycloak instance in Docker container:
docker run -d --name docker-keycloak
          -e KEYCLOAK_USER=admin         ①
          -e KEYCLOAK_PASSWORD=admin     ①
          -e DB_VENDOR=h2                ②
          -p 9080:8080                   ③
          -p 8443:8443                   ③
          -p 9990:9990                   ③
          jboss/keycloak:11.0.0           ④
  • ① By default, there is no admin user created, so you won’t be able to log in to the admin console. To create an admin account, you need to use environment variables to pass in an initial username and password.

  • ② Use H2 as the Keycloak database.

  • ③ List the exposed ports.

  • ④ This is based on Keycloak 11.0.0.

Open http://localhost:9080 to access the welcome page:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figee_HTML.jpg

Next, click Administration Console to authenticate to the console. Use the admin credentials for the username and password:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figef_HTML.jpg

Next, you need to create a new realm . Start by clicking Add Realm:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figeg_HTML.png
What is a Keycloak Realm?

A realm is the core concept in Keycloak. A realm manages a set of users, credentials, roles, and groups. A user belongs to and logs in to a realm. Realms are isolated from one another and can only manage and authenticate the users that they control.

When you boot up Keycloak for the first time, it creates a predefined realm for you. This initial realm is the master realm. It is the highest level in the hierarchy of realms. Admin accounts in this realm have permissions to view and manage any other realm created on the server instance. When you define your initial admin account, you create an account in the master realm. Your initial login to the admin console will also be via the master realm.

You will land next on the first step of creating a new realm. Call the realm quarkushop-realm then click Create:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figeh_HTML.jpg

Next, you will get the quarkushop-realm configuration page:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figei_HTML.jpg

In the quarkushop-realm, you need to define the roles that you will be using: the user and admin roles. To do this, simply go to Roles ➤Add Role:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figej_HTML.jpg

Now you can create the admin role :

../images/509649_1_En_6_Chapter/509649_1_En_6_Figek_HTML.jpg

Create the user role as well:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figel_HTML.jpg

Now that you have created the roles, you need to create the users. Just go to the Users menu:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figem_HTML.jpg

../images/509649_1_En_6_Chapter/509649_1_En_6_Figen_HTML.gif Sometimes on this screen, the list of users isn’t loaded when you open the page. If this happens, just click View All Users to load the list.

Click Add User to create the users (for this example, Nebrass, Jason, and Marie):

../images/509649_1_En_6_Chapter/509649_1_En_6_Figeo_HTML.jpg

Then after clicking Save, click the Credentials tab, where you will define the user passwords.

../images/509649_1_En_6_Chapter/509649_1_En_6_Figep_HTML.gif Don’t forget to set the Temporary to OFF to prevent Keycloak from asking you to update the password on the first login. Then, click Set Password:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figeq_HTML.jpg

Next, go to the Role Mappings tab and add all the roles to Nebrass. ../images/509649_1_En_6_Chapter/509649_1_En_6_Figer_HTML.gif Yes! I’ll be the admin of this app. ../images/509649_1_En_6_Chapter/509649_1_En_6_Figes_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figet_HTML.jpg

Now create the Jason user and define a password:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figeu_HTML.jpg

Add the user role to Jason:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figev_HTML.jpg

Next, create the Marie user and define a password:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figew_HTML.jpg

Add the user role to Marie:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figex_HTML.jpg

You are finished configuring roles and users! Now, click Clients to list the realm clients.

../images/509649_1_En_6_Chapter/509649_1_En_6_Figey_HTML.jpg
What is a Realm Client?

Clients are entities that can request Keycloak to authenticate a user. Most often, clients are applications and services that want to use Keycloak to secure themselves and provide a single sign-on solution. Clients can also be entities that just want to request identity information or an access token so that they can securely invoke other services on the network that are secured by Keycloak.

Click Create to add a new client with the following settings:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figez_HTML.jpg
  • Client ID: quarkushop

  • Client protocol: openid-connect

  • Root URL: http://localhost:8080/

Click Save to get the client configuration screen:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figfa_HTML.jpg

It’s mandatory to define a protocol mapper for your client.

What is a Protocol Mapper?

Protocol mappers perform transformations on tokens and documents. They can do things like map user data to protocol claims and transform any requests going between the client and auth server.

Click the Mappers tab to configure a mapper:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figfb_HTML.jpg

Then click Create to add a new mapper:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figfc_HTML.jpg

../images/509649_1_En_6_Chapter/509649_1_En_6_Figfd_HTML.gifWhy Do We Define Token Claim Name as Groups?

We will use the protocol mapper to map the User Realm role (which can be user or admin) to a property called groups, which we will add as a plain string to the ID token, the access token, and the userinfo.

The choice of groups is based on the MpJwtValidator Java class from the Quarkus SmallRye JWT library (which is the Quarkus implementation for managing the JWT); where we define the SecurityIdentity roles using jwtPrincipal.getGroups():
JsonWebToken jwtPrincipal = parser.parse(request.getToken().getToken());
uniEmitter.complete(
        QuarkusSecurityIdentity.builder().setPrincipal(jwtPrincipal)
            .addRoles(jwtPrincipal.getGroups())
            .addAttribute(SecurityIdentity.USER_ATTRIBUTE, jwtPrincipal)
            .build()
);

This is why we defined our mapper to affect the User roles to the Groups property embedded in the JWT token. ../images/509649_1_En_6_Chapter/509649_1_En_6_Figfe_HTML.gif

You can now test if the Keycloak instance is running as expected. Simply request an access_token using the cURL command :
curl -X POST http://localhost:9080/auth/realms/quarkushop-realm/protocol/openid-connect/token
      -H 'content-type: application/x-www-form-urlencoded'
      -d 'client_id=quarkushop'
      -d 'username=nebrass'
      -d 'password=password'
      -d 'grant_type=password' | jq '.'

../images/509649_1_En_6_Chapter/509649_1_En_6_Figff_HTML.gif The URL http://localhost:9080/auth/realms/quarkushop-realm/protocol/openid-connect/token is composed of:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figfg_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figfh_HTML.gif The jq is a lightweight and flexible command-line JSON processor that I’m using to format the JSON output.

You will get a JSON response like this:
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJKcmlxWGQzYVBNOS13djhXUmVJekZkRnRJa3Z1WG5uNDd4a0JmTl95R19zIn0.eyJleHAiOjE1OTc0OTEwNTIsImlhdCI6MTU5NzQ5MDc1MiwianRpIjoiZmIxZmQxOWMtNWJlMC00YTgwLWExOTUtOTAxZjFkOTI3NDI5IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDgwL2F1dGgvcmVhbG1zL3F1YXJrdXNob3AtcmVhbG0iLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMzU1ODc3YWQtMjY3Ny00ODJiLWE5NWYtYTI4ZjdmZGI1OTk5IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoicXVhcmt1c2hvcCIsInNlc3Npb25fc3RhdGUiOiJjM2E4ZmU3Mi02MzRmLTRiNmUtYTZkMS03MTkyOGI2YTBlN2YiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJhZG1pbiIsInVtYV9hdXRob3JpemF0aW9uIiwidXNlciJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJuYW1lIjoiTmVicmFzcyBMYW1vdWNoaSIsImdyb3VwcyI6WyJvZmZsaW5lX2FjY2VzcyIsImFkbWluIiwidW1hX2F1dGhvcml6YXRpb24iLCJ1c2VyIl0sInByZWZlcnJlZF91c2VybmFtZSI6Im5lYnJhc3MiLCJnaXZlbl9uYW1lIjoiTmVicmFzcyIsImZhbWlseV9uYW1lIjoiTGFtb3VjaGkiLCJlbWFpbCI6Im5lYnJhc3NAcXVhcmt1c2hvcC5zdG9yZSJ9.HZmicWhE9V8g74of9KGcZOVGvwC_oo2zs4-ElBBuV6XSWDUoiFLJVkSUzOV4WFzwvsM7V7_aZRzihZqq6QTtezweyhZIauo3pjmmtbMnq16WUFV-4oJWzk3P_6T5y74sh93aPuQtnw5hSQ4L68RjwQ6HIcaHJFkqrh6fX7uy0ZiHuPnRzhv38uQrD9YMC_z3tApWKTS2TA9igizZrlJCDfTdfiThUDuXEgOmw-pffYx1BASfL14O0c0apGPqirNkSgSrCpuFvikXlRdeu3YnI1JQ6S7Jn-qQI-bdCD5M0_ynaUiJn_p6sZqI6ioSmLGyA__S5J7nj_BO--fdIl0lUA",
  "expires_in": 300,
  "refresh_expires_in": 1800,
  "refresh_token": "...",
  "token_type": "bearer",
  "not-before-policy": 0,
  "session_state": "c3a8fe72-634f-4b6e-a6d1-71928b6a0e7f",
  "scope": "email profile"
}

../images/509649_1_En_6_Chapter/509649_1_En_6_Figfi_HTML.gif For prettier output, I omitted the value of refresh_token, as it’s very long and useless.

Copy the access_token value. Then go to jwt.io and paste the JWT access_token there to decode it:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figfj_HTML.jpg

Note that there is a JSON attribute called groups that holds the Roles array that we assigned to Nebrass in Keycloak.

Good! ../images/509649_1_En_6_Chapter/509649_1_En_6_Figfk_HTML.gif Keycloak is running and working like expected. You’ll now begin implementing security on the Java side.

Implementing the auth2 Java Components in QuarkuShop

Java Configuration Side
The first step is to add the Quarkus SmallRye JWT dependency to QuarkuShop:
./mvnw quarkus:add-extension -Dextensions="io.quarkus:quarkus-smallrye-jwt"
Next, add the Security configuration to the application.properties:
1  ### Security
2  quarkus.http.cors=true  ①
3  # MP-JWT Config
4  mp.jwt.verify.issuer=http://localhost:9080/auth/realms/quarkushop-realm   ②
5  mp.jwt.verify.publickey.location=http://localhost:9080/auth/realms/quarkushop-realm/protocol/openid-connect/certs   ③
6  # Keycloak Configuration
7  keycloak.credentials.client-id=quarkushop   ④
  • ① Enables the HTTP CORS filter.

What is CORS?

Cross-Origin Resource Sharing (CORS) is a mechanism that allows restricted resources on a web page to be requested from another domain outside the domain from which the first resource was served.

  • ② Config property specifies the value of the iss (issuer) claim of the JWT token that the server will accept as valid. We already got this value in the iss field of the decrypted JWT token.

  • ③ Config property allows for an external or internal location of the public key to be specified. The value may be a relative path or an URL. When using Keycloak as the identity manager, the value will be the mp.jwt.verify.issuer plus /protocol/openid-connect/certs.

  • ④ A custom property with a key called keycloak.credentials.client-id, which holds the value quarkushop, which is the Realm client ID.

Java Source Code Side

In the Java code, you need to protect the REST APIs based on the authorization matrix that we created for each service.

Let’s begin with the Carts REST API, where all operations are allowed for users and admins. Only the authenticated subjects are allowed on the Carts REST API. To satisfy this requirement, we have an @Authenticated annotation from the io.quarkus.security package, which will grant access only to authenticated subjects.

../images/509649_1_En_6_Chapter/509649_1_En_6_Figfl_HTML.gif To differentiate between an application user and the role user, I will refer to an application user as a subject. The term subject came in the old good Java Authentication and Authorization Service (JAAS) to represent the caller that is making a request to access a resource. A subject may be any entity, including as a person or service.

As all the operations in the CartResource require an authenticated subject, you have to annotate the CartResource class with @Authenticated so it will be applied to all the operations. See Listing 6-1.
@Authenticated
@Path("/carts")
@Tags(value = @Tag(name = "cart", description = "All the cart methods"))
public class CartResource {
    ...
}
Listing 6-1

com.targa.labs.quarkushop.web.CartResource

The next REST API is the Category API, where only the admin can create and delete categories. All the other operations are allowed by everyone (the admin, user, and anonymous). To grant access to a specific role only, use the @RolesAllowed annotation from the javax.annotation.security package.

../images/509649_1_En_6_Chapter/509649_1_En_6_Figfm_HTML.gifJavaDoc of javax.annotation.security.RolesAllowed

@RolesAllowed: Specifies the list of security roles permitted to access the method(s) in an application.

The value of the @RolesAllowed annotation is a list of security role names. This annotation can be specified on a class or on methods:
  • Specifying it at a class level means that it applies to all the operations in the class.

  • Specifying it on a method means that it applies to that method only.

  • If applied to the class and operations levels, the method value overrides the class value when the two conflict.

Based on the JavaDoc, we will pass the desired role as the value of the annotation. Apply the @RolesAllowed("admin") annotation to the operations where you create and delete categories:
@Path("/categories")
@Tags(value = @Tag(name = "category", description = "All the category methods"))
public class CategoryResource {
    ...
    @RolesAllowed("admin")
    @POST
        public CategoryDto create(CategoryDto categoryDto) {
        return this.categoryService.create(categoryDto);
    }
    @RolesAllowed("admin")
    @DELETE
    @Path("/{id}")
    public void delete(@PathParam("id") Long id) {
        this.categoryService.delete(id);
    }
}

For the Customer REST API, only the admin is allowed to access all the services. Annotating the CustomerResource class with @RolesAllowed("admin") will apply this policy.

Next is the Order REST API, where the authenticated subjects can access all the operations, except for the findAll() method , which is allowed only for admin. So we combine the two @Authenticated annotations in the class level and @RolesAllowed on the method requiring a specific role:
@Authenticated
@Path("/orders")
@Tag(name = "order", description = "All the order methods")
public class OrderResource {
    ...
    @RolesAllowed("admin")
    @GET
    public List<OrderDto> findAll() {
        return this.orderService.findAll();
    }
    ...
}

../images/509649_1_En_6_Chapter/509649_1_En_6_Figfn_HTML.gif While we are using @RolesAllowed in the local method, we are overriding the policy applied by the class level @Authenticated.

After OrderResource, we will deal with the OrderItem REST API, where only authenticated subjects are allowed to access all the services. Annotating the OrderItemResource class with @Authenticated will apply this policy.

The PaymentResource grants access only to authenticated subjects, except that deleting and listing all payments are granted only to the admin:
@Authenticated
@Path("/payments")
@Tag(name = "payment", description = "All the payment methods")
public class PaymentResource {
    ...
    @RolesAllowed("admin")
    @GET
    public List<PaymentDto> findAll() {
        return this.paymentService.findAll();
    }
    @RolesAllowed("admin")
    @DELETE
    @Path("/{id}")
    public void delete(@PathParam("id") Long id) {
        this.paymentService.delete(id);
    }
    ...
}
For the Product REST API, all operations are allowed by everyone, except for creating and deleting products, which are allowed only by the admin:
@Path("/products")
@Tag(name = "product", description = "All the product methods")
public class ProductResource {
    ...
    @RolesAllowed("admin")
    @POST
        public ProductDto create(ProductDto productDto) {
        return this.productService.create(productDto);
    }
    @RolesAllowed("admin")
    @DELETE
    @Path("/{id}")
    public void delete(@PathParam("id") Long id) {
        this.productService.delete(id);
    }
    ...
}
Finally, the last REST API is ReviewResource:
@Path("/reviews")
@Tag(name = "review", description = "All the review methods")
public class ReviewResource {
    ...
    @Authenticated
    @POST
    @Path("/product/{id}")
        public ReviewDto create(ReviewDto reviewDto, @PathParam("id") Long id) {
        return this.reviewService.create(reviewDto, id);
    }
    @RolesAllowed("admin")
    @DELETE
    @Path("/{id}")
    public void delete(@PathParam("id") Long id) {
        this.reviewService.delete(id);
    }
}

Great! You have applied the first-level authorization layer! ../images/509649_1_En_6_Chapter/509649_1_En_6_Figfo_HTML.gif

Don’t be too confident, though. ../images/509649_1_En_6_Chapter/509649_1_En_6_Figfp_HTML.gif This implementation leaks more levels of authorization. For example, users are authorized to delete Orders or OrderItems, but we can’t verify that the authenticated subject is only deleting his own Orders or OrderItems and not those of other users. We know how to define access rules based on roles, but how can we gather more information about the authenticated subject?

For this purpose, we will create a new REST API called UserResource. When invoked, UserResource will return the current authenticated subject information as a response.

UserResource will look like this:
@Path("/user")
@Authenticated
@Tag(name = " user", description = "All the user methods")
public class UserResource {
    @Inject
    JsonWebToken jwt;                                           ①
    @GET
    @Path("/current/info")
    public JsonWebToken getCurrentUserInfo() {                  ②
        return jwt;
    }
    @GET
    @Path("/current/info/claims")
    public Map<String, Object> getCurrentUserInfoClaims() {     ③
        return jwt.getClaimNames()
                .stream()
                .map(name -> Map.entry(name, jwt.getClaim(name)))
                .collect(Collectors.toMap(
                        entry -> entry.getKey(),
                        entry -> entry.getValue())
                );
    }
}
  • ① Inject a JsonWebToken, which is an implementation of the specification that defines the use of JWT as a bearer token. This injection will use an instance of the JWT token in the incoming request.

  • ② The JsonWebToken will return the injected JWT token.

  • ③ The getCurrentUserInfoClaims() method will return the list of the available claims and their respective values embedded in the JWT token. The claims will be extracted from the JWT token instance injected in the UserResource.

Let’s test this new REST API. Start by getting a new access_token:
export access_token=$(curl -X POST http://localhost:9080/auth/realms/quarkushop-realm/protocol/openid-connect/token
      -H 'content-type: application/x-www-form-urlencoded'
      -d 'client_id=quarkushop'
      -d 'username=nebrass'
      -d 'password=password'
      -d 'grant_type=password' | jq --raw-output '.access_token')
Then, invoke the /user/current/info REST API:
curl -X GET -H "Authorization: Bearer $access_token" http://localhost:8080/api/user/current/info | jq '.'
You will get as a response the JWT token:
{
  "audience": [
    "account"
  ],
  "claimNames": [
    "sub", "resource_access", "email_verified", "allowed-origins", "raw_token", "iss",
    "groups", "typ", "preferred_username", "given_name", "aud", "acr", "realm_access",
    "azp", "scope", "name", "exp", "session_state", "iat", "family_name", "jti", "email"
  ],
  "expirationTime": 1597530481,
  "groups": ["offline_access", "admin", "uma_authorization", "user"],
  "issuedAtTime": 1597530181,
  "issuer": "http://localhost:9080/auth/realms/quarkushop-realm",
  "name": "nebrass",
  "rawToken": "eyJhbGci...L5A",
  "subject": "355877ad-2677-482b-a95f-a28f7fdb5999",
  "tokenID": "7416ee6e-e74c-45ae-bf85-8889744eaacf"
}
Good, we have the available claims. Let’s get their respective values:
{
  "sub": "355877ad-2677-482b-a95f-a28f7fdb5999",
  "resource_access": {
    "account": {
      "roles": ["manage-account", "manage-account-links", "view-profile"]
    }
  },
  "email_verified": true,
  "allowed-origins": ["http://localhost:8080"],
  "raw_token": "eyJhbGci...L5A",
  "iss": "http://localhost:9080/auth/realms/quarkushop-realm",
  "groups": ["offline_access", "admin", "uma_authorization", "user"],
  "typ": "Bearer",
  "preferred_username": "nebrass",
  "given_name": "Nebrass",
  "aud": ["account"],
  "acr": "1",
  "realm_access": {
    "roles": ["offline_access", "admin", "uma_authorization", "user"]
  },
  "azp": "quarkushop",
  "scope": "email profile",
  "name": "Nebrass Lamouchi",
  "exp": 1597530481,
  "session_state": "68069099-f534-434f-8d08-d8d75b8ff1c6",
  "iat": 1597530181,
  "family_name": "Lamouchi",
  "jti": "7416ee6e-e74c-45ae-bf85-8889744eaacf",
  "email": "[email protected]"
}
Wonderful! ../images/509649_1_En_6_Chapter/509649_1_En_6_Figfq_HTML.gif Now you know how to access the security details that you can use to identify the authenticated subjects exactly. By the way, you can have the same information about the JWT token. You can get the same JWT token an alternative way:
@GET()
@Path("/current/info-alternative")
public Principal getCurrentUserInfoAlternative(@Context SecurityContext ctx) {
    return ctx.getUserPrincipal();
}

The @Context SecurityContext ctx passed as the method parameter is used to inject the SecurityContext in the current context.

../images/509649_1_En_6_Chapter/509649_1_En_6_Figfr_HTML.gifSecurityContext is an injectable interface that provides access to security-related information like the Principal.

../images/509649_1_En_6_Chapter/509649_1_En_6_Figfs_HTML.gif The Principal interface represents the abstract notion of a principal, which can be used to represent any entity, such as an individual, a corporation, or a login ID. In this case, the principal is the JWT token. ../images/509649_1_En_6_Chapter/509649_1_En_6_Figft_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figfu_HTML.gif I will not dig into the full implementation of the authorization layer. I will stop here; otherwise, I would need to spend two more chapters just on this content.

The last step of the security part is to add a REST API that returns an access_token for a given username and password. This can be useful, especially with the Swagger UI. Speaking of Swagger UI, we need to make it aware of our Security layer.

We will start by creating the TokenService, which will be the REST client that makes the request of the access_token from Keycloak.

As we are using Java 11, we can enjoy one of its great new features: the brand new HTTP client.

As you did before using the cURL command, you need to use the Java 11 HTTP client to request an access_token:
@RequestScoped                              ①
public class TokenService {
    @ConfigProperty(name = "mp.jwt.verify.issuer", defaultValue = "undefined")
    Provider<String> jwtIssuerUrlProvider;  ②
    @ConfigProperty(name = "keycloak.credentials.client-id", defaultValue = "undefined")
    Provider<String> clientIdProvider;      ③
    public String getAccessToken(String userName, String password)
                        throws IOException, InterruptedException {
        String keycloakTokenEndpoint =
                    jwtIssuerUrlProvider.get() + "/protocol/openid-connect/token";
        String requestBody = "username=" + userName + "&password=" + password +
                    "&grant_type=password&client_id=" + clientIdProvider.get();
        if (clientSecret != null) {
            requestBody += "&client_secret=" + clientSecret;
        }
        HttpClient client = HttpClient.newBuilder().build();
        HttpRequest request = HttpRequest.newBuilder()
                .POST(BodyPublishers.ofString(requestBody))
                .uri(URI.create(keycloakTokenEndpoint))
                .header("Content-Type", "application/x-www-form-urlencoded")
                .build();
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
        String accessToken = "";
        if (response.statusCode() == 200) {
            ObjectMapper mapper = new ObjectMapper();
            try {
                accessToken = mapper.readTree(response.body()).get("access_token").textValue();
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            throw new UnauthorizedException();
        }
        return accessToken;
    }
}
  • ① The annotation creates an instance of TokenService for every HTTP request.

  • ② Gets the mp.jwt.verify.issuer property value to build the Keycloak token’s endpoint URL.

  • ③ Gets the keycloak.credentials.client-id property value, which is needed to request the access_token.

../images/509649_1_En_6_Chapter/509649_1_En_6_Figfv_HTML.gif We are getting the property value as Provider<String> and not String to avoid failing the native image build.

Next, add the getAccessToken() method:
@Path("/user")
@Authenticated
@Tag(name = " user", description = "All the user methods")
public class UserResource {
    @Inject JsonWebToken jwt;
    @POST
    @PermitAll
    @Path("/access-token")
    @Produces(MediaType.TEXT_PLAIN)
    public String getAccessToken(@QueryParam("username") String username,
                                @QueryParam("password") String password)
                        throws IOException, InterruptedException {
        return tokenService.getAccessToken(username, password);
    }
    ...
}

Excellent! ../images/509649_1_En_6_Chapter/509649_1_En_6_Figfw_HTML.gif You can test the new method from the Swagger UI:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figfx_HTML.jpg

Yippee! ../images/509649_1_En_6_Chapter/509649_1_En_6_Figfy_HTML.gif You now need to find a way to make Swagger UI apply the access_token to request all the REST APIs.

I thought this would be a long trip, but it was a very easy task. Just 10 lines of code are enough to make the party: ../images/509649_1_En_6_Chapter/509649_1_En_6_Figfz_HTML.gif
@SecurityScheme(
        securitySchemeName = "jwt",             ①
        description = "JWT authentication with bearer token",
        type = SecuritySchemeType.HTTP,         ①
        in = SecuritySchemeIn.HEADER,           ①
        scheme = "bearer",                      ①
        bearerFormat = "Bearer [token]")        ①
@OpenAPIDefinition(
        info = @Info(                           ②
                title = "QuarkuShop API",
                description = "Sample application for the book 'Playing with Java Microservices with Quarkus and Kubernetes'",
                contact = @Contact(name = "Nebrass Lamouchi", email = "[email protected]", url = "https://blog.nebrass.fr"),
                version = "1.0.0-SNAPSHOT"
        ),
        security = @SecurityRequirement(name = "JWT") ③
)
public class OpenApiConfig extends Application {
}
  • @SecurityScheme defines a security scheme that can be used by the OpenAPI operations.

  • ② Add a description for the OpenAPI definition.

  • ③ Link the created security schema jwt to the OpenAPI definition.

Check the Swagger UI again:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figga_HTML.jpg

Note that there is the lock icon ../images/509649_1_En_6_Chapter/509649_1_En_6_Figgb_HTML.gif.

Use the getAccessToken() operation to create an access_token and then click Authorize ../images/509649_1_En_6_Chapter/509649_1_En_6_Figgc_HTML.gif to pass the generated access_token to the SecurityScheme. Finally, click Authorize:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figgd_HTML.jpg

Now, when you click any operation, Swagger UI will include the access_token as a bearer to every request.

Excellent! You now have the Keycloak and the Security Java components. Next, you need to refactor the tests to be aware of the Security layer.

Update the Integration Tests to Support auth2

For the tests, you need to dynamically provision the Keycloak instance, as you did for the database using the Testcontainers library.

We will use the same library to provision Keycloak. We will not use a plain Docker container as we did with PostgreSQL, because there, Flyway populated the database for us. For Keycloak, we don’t have a built-in mechanism that will create the Keycloak realms for us. The only possible solution is to use a Docker Compose file that will import a sample Keycloak realms file. Fortunately, Testcontainers has great support to provision Docker Compose files.

To provision the containers from a Docker Compose file using TestContainers, use this:
public static DockerComposeContainer KEYCLOAK = new DockerComposeContainer(
    new File("src/main/docker/keycloak-test.yml"))
       .withExposedService("keycloak_1", 9080,
           Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)));
We will use a sample Keycloak realm that contains the following:
  • Two users:
    • Admin (username admin, password test, role admin)

    • Test (username user, password test, role user)

  • The Keycloak client has a client-id=quarkus-client: and `secret=mysecret.

The QuarkusTestResourceLifecycleManager will communicate with the provisioned Keycloak instance using the TokenService that we created, and it will use the sample Realm credentials to get access_tokens that we will use in the integration tests.

We will get two access_tokens: one for the admin role and one for the user role. We will store them, along with mp.jwt.verify.publickey.location and mp.jwt.verify.issuer, as system properties in the current test scope.

The custom QuarkusTestResourceLifecycleManager will look like this:
public class KeycloakRealmResource implements QuarkusTestResourceLifecycleManager {
     @ClassRule
     public static DockerComposeContainer KEYCLOAK = new DockerComposeContainer(
           new File("src/main/docker/keycloak-test.yml"))
          .withExposedService("keycloak_1",
                    9080,
                    Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)));
       @Override
       public Map<String, String> start() {
              KEYCLOAK.start();
             String jwtIssuerUrl = String.format(
                    "http://%s:%s/auth/realms/quarkus-realm",
                    KEYCLOAK.getServiceHost("keycloak_1", 9080),
                    KEYCLOAK.getServicePort("keycloak_1", 9080)
             );
            TokenService tokenService = new TokenService();
            Map<String, String> config = new HashMap<>();
            try {
                 String adminAccessToken = tokenService.getAccessToken(jwtIssuerUrl,
                      "admin", "test", "quarkus-client", "mysecret"
                );
                String testAccessToken = tokenService.getAccessToken(jwtIssuerUrl,
                      "test", "test", "quarkus-client", "mysecret"
                );
               config.put("quarkus-admin-access-token", adminAccessToken);
               config.put("quarkus-test-access-token", testAccessToken);
           } catch (IOException | InterruptedException e) {
                  e.printStackTrace();
          }
          config.put("mp.jwt.verify.publickey.location", jwtIssuerUrl + "/protocol/openidconnect/certs");
          config.put("mp.jwt.verify.issuer", jwtIssuerUrl);
          return config;
      }
     @Override
      public void stop() {
              KEYCLOAK.stop();
     }
}

We will use it with the @QuarkusTestResource(KeycloakRealmResource.class) annotation.

In the @BeforeAll method, we get the access_tokens from the systems properties to make them ready to be used in tests. A typical test skull will look like this:
@QuarkusTest
@QuarkusTestResource(TestContainerResource.class)
@QuarkusTestResource(KeycloakRealmResource.class)
class CategoryResourceTest {
     static String ADMIN_BEARER_TOKEN;
     static String USER_BEARER_TOKEN;
     @BeforeAll
     static void init() {
       ADMIN_BEARER_TOKEN = System.getProperty("quarkus-admin-access-token");
       USER_BEARER_TOKEN = System.getProperty("quarkus-test-access-token");
    }
    ...
}
To use these tokens in a test:
@Test
void testFindAllWithAdminRole() {
   given().when()
          .header(HttpHeaders.AUTHORIZATION, "Bearer " + ADMIN_BEARER_TOKEN)
          .get("/carts")
          .then()
          .statusCode(OK.getStatusCode())
          .body("size()", greaterThan(0));
}
You need to test and verify the authorization rules in tests, for example, to verify that a given profile is not Unauthorized:
@Test
void testFindAll() {
    get("/carts").then()
          .statusCode(UNAUTHORIZED.getStatusCode());
}
To verify that, for a given request, the subject is not allowed on a REST API, use this:
@Test
void testDeleteWithUserRole() {
   given().when()
          .header(HttpHeaders.AUTHORIZATION, "Bearer " + USER_BEARER_TOKEN)
          .delete("/products/1")
          .then()
          .statusCode(FORBIDDEN.getStatusCode());
}

Good! I implemented all the tests; you can find them in my ../images/509649_1_En_6_Chapter/509649_1_En_6_Figge_HTML.gif GitHub repository. ../images/509649_1_En_6_Chapter/509649_1_En_6_Figgf_HTML.gif

Adding Keycloak to the Production Environment

The last step is to add the production Keycloak entries to Docker Compose in the Production VM. We also need to add the Production realms.

You can export the realm from the local Keycloak instance in very few steps.

To export the realm from the local Keycloak container (called docker-keycloak), use this:
$docker exec -it docker-keycloak bash
bash-4.4$ cd /opt/jboss/keycloak/bin/
bash-4.4$ mkdir backup
bash-4.4$ ./standalone.sh -Djboss.socket.binding.port-offset=1000
      -Dkeycloak.migration.realmName=quarkushop-realm
      -Dkeycloak.migration.action=export
      -Dkeycloak.migration.provider=dir
      -Dkeycloak.migration.dir=./backup/
To copy the stored realm from the docker-keycloak container to a local directory, use this:
$ mkdir ~/keycloak-realms
$ docker cp docker-keycloak:/opt/jboss/keycloak/bin/backup ~/keycloak-realms

You will get two Keycloak realm files—quarkushop-realm-realm.json and quarkushop-realm-users-0.json—in ~/keycloak-realms.

You need to edit the quarkushop-realm-realm.json file and change sslRequired from external to none:
{
    ...
    "sslRequired": "none",
    ...
}

../images/509649_1_En_6_Chapter/509649_1_En_6_Figgg_HTML.gif The "sslRequired": "none" property disables the required SSL certificate for any request.

Then, copy the two files—quarkushop-realm-realm.json and quarkushop-realm-users-0.json—to the /opt/realms directory in the Azure VM instance, which is the Production environment.

Here we are in the Azure VM instance. ../images/509649_1_En_6_Chapter/509649_1_En_6_Figgh_HTML.gif We will finish the last step: adding the Keycloak service to the /opt/docker-compose.yml file:
 1 version: '3'
 2 services:
 3   quarkushop:
 4     image: nebrass/quarkushop-monolithic-application:latest
 5     environment:
 6       - QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://postgresql-db:5432/demo
 7       -
  MP_JWT_VERIFY_PUBLICKEY_LOCATION=http://51.103.50.23:9080/auth/realms/quarkushoprealm/protocol/openid-connect/certs   ①
 8       - MP_JWT_VERIFY_ISSUER=http://51.103.50.23:9080/auth/realms/quarkushop-realm ①
 9    ports:
10      - 8080:8080
11   postgresql-db:
12     image: postgres:13
13     volumes:
14       - /opt/postgres-volume:/var/lib/postgresql/data
15     environment:
16       - POSTGRES_USER=developer
17       - POSTGRES_PASSWORD=p4SSW0rd
18       - POSTGRES_DB=demo
19       - POSTGRES_HOST_AUTH_METHOD=trust
20    ports:
21      - 5432:5432
22   keycloak:                                     ②
23     image: jboss/keycloak:latest
24     command:
25       [
26         '-b','0.0.0.0',
27
28         '-Dkeycloak.migration.action=import',
29         '-Dkeycloak.migration.provider=dir',
30         '-Dkeycloak.migration.dir=/opt/jboss/keycloak/realms',
31         '-Dkeycloak.migration.strategy=OVERWRITE_EXISTING',
32         '-Djboss.socket.binding.port-offset=1000',
33         '-Dkeycloak.profile.feature.upload_scripts=enabled',
34       ]
35
36
37   volumes:
38     - ./realms:/opt/jboss/keycloak/realms
39   environment:
40     - KEYCLOAK_USER=admin     ③
41     - KEYCLOAK_PASSWORD=admin ③
42     - DB_VENDOR=POSTGRES      ④
43     - DB_ADDR=postgresql-db   ⑤
44     - DB_DATABASE=demo        ⑥
45     - DB_USER=developer       ⑥
46     - DB_SCHEMA=public        ⑥
47     - DB_PASSWORD=p4SSW0rd    ⑥
48   ports:
49     - 9080:9080
50   depends_on:                 ⑦
51     - postgresql-db           ⑦
  • ① Overrides the MP_JWT_VERIFY_PUBLICKEY_LOCATION and MP_JWT_VERIFY_ISSUER properties to ensure that the application points to the Azure VM instance instead of the localhost.

  • ② Adds the Keycloak Docker service.

  • ③ Defines the Keycloak cluster’s username and password.

  • ④ Defines the Keycloak Database vendor as PostgreSQL, as we have a PostgreSQL DB instance in our services.

  • ⑤ Defines the DB host as the postgresql-db, which will be resolved dynamically by Docker with the service IP.

  • ⑥ Defines the Keycloak database credentials as the same as the PostgreSQL credentials.

  • ⑦ Expresses the dependency between the Keycloak and PostgreSQL services.

../images/509649_1_En_6_Chapter/509649_1_En_6_Figgi_HTML.gif As you can see, all the credentials are listed as plain text in the docker-compose.yml file.

You can protect these credentials using Docker Secrets. Each password will be stored in a Docker secret.
docker service create --name POSTGRES_USER --secret developer
docker service create --name POSTGRES_PASSWORD --secret p4SSW0rd
docker service create --name POSTGRES_DB --secret demo
docker service create --name KEYCLOAK_USER --secret admin
docker service create --name KEYCLOAK_PASSWORD --secret admin

../images/509649_1_En_6_Chapter/509649_1_En_6_Figgj_HTML.gif Unfortunately, this feature is available only for Docker Swarm clusters.

To protect the credentials, store them in an ~/.env file on the Azure VM instance:
POSTGRES_USER=developer
POSTGRES_PASSWORD=p4SSW0rd
POSTGRES_DB=demo
KEYCLOAK_USER=admin
KEYCLOAK_PASSWORD=admin
Then, change docker-compose.yml to use the ~/.env elements:
version: '3'
services:
   quarkushop:
     image: nebrass/quarkushop-monolithic-application:latest
     environment:
       - QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://postgresql-db:5432/${POSTGRES_DB}
       - MP_JWT_VERIFY_PUBLICKEY_LOCATION=http://51.103.50.23:9080/auth/realms/quarkushop-realm/protocol/openid-connect/certs
       - MP_JWT_VERIFY_ISSUER=http://51.103.50.23:9080/auth/realms/quarkushop-realm
     ports:
       - 8080:8080
   postgresql-db:
     image: postgres:13
     volumes:
       - /opt/postgres-volume:/var/lib/postgresql/data
     environment:
       - POSTGRES_USER=${POSTGRES_USER}
       - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
       - POSTGRES_DB=${POSTGRES_DB}
       - POSTGRES_HOST_AUTH_METHOD=trust
     ports:
       - 5432:5432
   keycloak:
     image: jboss/keycloak:latest
     command:
      [
       '-b',
       '0.0.0.0',
       '-Dkeycloak.migration.action=import',
       '-Dkeycloak.migration.provider=dir',
       '-Dkeycloak.migration.dir=/opt/jboss/keycloak/realms',
       '-Dkeycloak.migration.strategy=OVERWRITE_EXISTING',
       '-Djboss.socket.binding.port-offset=1000',
       '-Dkeycloak.profile.feature.upload_scripts=enabled',
      ]
     volumes:
       - ./realms:/opt/jboss/keycloak/realms
     environment:
       - KEYCLOAK_USER=${KEYCLOAK_USER}
       - KEYCLOAK_PASSWORD=${KEYCLOAK_PASSWORD}
       - DB_VENDOR=POSTGRES
       - DB_ADDR=postgresql-db
       - DB_DATABASE=${POSTGRES_DB}
       - DB_USER=${POSTGRES_USER}
       - DB_SCHEMA=public
       - DB_PASSWORD=${POSTGRES_PASSWORD}
     ports:
       - 9080:9080
     depends_on:
       - postgresql-db

Good! The Docker Compose services are ready to go! ../images/509649_1_En_6_Chapter/509649_1_En_6_Figgk_HTML.gif

We need just to add an exception to the Azure VM instance networking rules for the Keycloak port. In the Azure VM instance, go to the Networking section and add an exception for port 9080:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figgl_HTML.jpg

Excellent! ../images/509649_1_En_6_Chapter/509649_1_En_6_Figgm_HTML.gif The production environment has all the needed elements to deploy a new version of the QuarkuShop container without risking the PostgreSQL DB or the Keycloak cluster.

Go to the production Swagger UI and enjoy QuarkuShop: the greatest online store! ../images/509649_1_En_6_Chapter/509649_1_En_6_Figgn_HTML.gif

../images/509649_1_En_6_Chapter/509649_1_En_6_Figgo_HTML.jpg

Good! ../images/509649_1_En_6_Chapter/509649_1_En_6_Figgp_HTML.gif It’s time to move to the next anti-disaster layer: monitoring ../images/509649_1_En_6_Chapter/509649_1_En_6_Figgq_HTML.gif.

Implementing the Monitoring Layer

Security is not the only critical extra layer in an application. Metrics monitoring is also a very important layer that can prevent disasters. Imagine a situation where you have a super-secure, powerful application deployed on a server in the cloud. If you don’t regularly check the application metrics, the application may get out of resources without you even knowing.

Application monitoring is not just a mechanism of getting metrics; it includes analyzing the performance and the behavior of the different components. I will not cover all the different monitoring tools and practices in this chapter.

Instead, you will see how to implement two components:
  • The application status indicator, which is also known as the health check indicator.

  • The application metrics service, which is used to provide various metrics and statistics about an application.

Implementing Health Checks

Implementing health checks in Quarkus is a very easy task: you simply need to add the SmallRye Health extension. Yes! You just add one library to pom.xml! The magic will happen automatically!
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-health</artifactId>
</dependency>

SmallRye Health will automatically add the health check endpoints to your Quarkus application.

../images/509649_1_En_6_Chapter/509649_1_En_6_Figgr_HTML.gif For those who are used to Spring Boot, SmallRye Health is the equivalent of Actuator.

The new health check endpoints are:
  • /health/live: The application is up and running.

  • /health/ready: The application is ready to serve requests.

  • /health: Accumulating all health check procedures in the application.

Run the application and do a cURL GET on http://localhost:8080/api/health:
curl -X GET http://localhost:8080/api/health | jq '.'
You will get a JSON response:
{
  "status": "UP",
  "checks": [
    {
      "name": "Database connections health check",
      "status": "UP"
    }
  ]
}

This JSON response confirms that the application is correctly running, and there is one check that was made confirming that the database is UP.

All the health REST endpoints return a simple JSON object with two fields:
  • status: The overall result of all the health check procedures

  • checks: An array of individual checks

Out of the box, Quarkus includes a database check. Let’s create a health check for the Keycloak instance:
@Liveness
@ApplicationScoped
public class KeycloakConnectionHealthCheck implements HealthCheck {
    @ConfigProperty(name = "mp.jwt.verify.publickey.location", defaultValue = "false")
    private Provider<String> keycloakUrl;                                 ①
    @Override
    public HealthCheckResponse call() {
        HealthCheckResponseBuilder responseBuilder =
                HealthCheckResponse.named("Keycloak connection health check");                               ③
        try {
            keycloakConnectionVerification();                             ④
            responseBuilder.up();                                         ⑤
        } catch (IllegalStateException e) {
            // cannot access keycloak
            responseBuilder.down().withData("error", e.getMessage());                                              ⑤
        }
        return responseBuilder.build();                                   ⑥
    }
    private void keycloakConnectionVerification() {
        HttpClient httpClient = HttpClient.newBuilder()                   ②
                .connectTimeout(Duration.ofMillis(3000))
                .build();
        HttpRequest request = HttpRequest.newBuilder()
                .GET()
                .uri(URI.create(keycloakUrl.get()))
                .build();
        HttpResponse<String> response = null;
        try {
            response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        }
        if (response == null || response.statusCode() != 200) {
            throw new IllegalStateException("Cannot contact Keycloak");
        }
    }
}
  • ① You get the mp.jwt.verify.publickey.location property, which you will use as the Keycloak URL.

  • ② You instantiate the Java 11 HTTPClient with 3000 milliseconds of timeout.

  • ③ You define the name of the health check as Keycloak connection health check.

  • ④ You verify that the Keycloak URL is reachable and the response status code is HTTP 200.

  • ⑤ If the keycloakConnectionVerification() throws an exception, the health check status will be down.

  • ⑥ You build the health check response and send it back to the caller.

Let’s cURL again the /health endpoint:
curl -X GET http://localhost:8080/api/health | jq '.'
The new JSON response will be: ../images/509649_1_En_6_Chapter/509649_1_En_6_Figgs_HTML.gif
{
  "status": "UP",
  "checks": [
    {
      "name": "Keycloak connection health check",
      "status": "UP"
    },
    {
      "name": "Database connections health check",
      "status": "UP"
    }
  ]
}

Good! ../images/509649_1_En_6_Chapter/509649_1_En_6_Figgt_HTML.gif It’s as simple as that!

There is more! SmallRye Health provides a very useful Health UI at http://localhost:8080/api/health-ui/:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figgu_HTML.jpg

There is even a pooling service that can be configured to refresh the page in intervals you set:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figgv_HTML.jpg
This Health UI is very useful, but unfortunately it’s not activated by default in the prod profile. To enable it for every profile/environment, use this property:
### Health Check
quarkus.smallrye-health.ui.always-include=true

Excellent. ../images/509649_1_En_6_Chapter/509649_1_En_6_Figgw_HTML.gif It’s time to move to the next step, in order to implement the metrics service.

Implementing the Metrics Service

Metrics are important and critical monitoring data. Quarkus has many dedicated libraries for metrics exposure, along with a very rich toolset to build custom application metrics. No surprise, the first library is also from the SmallRye family. It’s called quarkus-smallrye-metrics and is used to expose metrics based on the MicroProfile specifications.

Starting with Quarkus v1.9, it’s not recommended to use SmallRye metrics. Quarkus officially adopted Micrometer as the new standard for its metrics. This adoption is based on cloud market trends and needs.

../images/509649_1_En_6_Chapter/509649_1_En_6_Figgx_HTML.gif To learn more about the transition from MicroProfile metrics to Micrometer metrics, see https://quarkus.io/blog/micrometer-metrics/.

The first step in implementing the metrics service is to add the quarkus-micrometer Maven dependency:
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-micrometer</artifactId>
</dependency>
This dependency will provide access to the Micrometer core metric types:
  • Counter: Counters are used to measure values that only increase. For example, to measure how many times a REST API was called.

  • Gauge: An indicator that shows a current value, like the number of created objects or threads.

  • Timer: Used to measure short-duration latencies and their frequencies.

  • Distribution Summary: Used to track the distribution of events. It is similar to a timer structurally, but records values that do not represent units of time. For example, a distribution summary could be used to measure the payload sizes of requests hitting a server.

All these objects need to be stored somewhere, which is the purpose of the meter registry. As Micrometer supports many monitoring systems (Prometheus, Azure Monitor, Stackdriver, Datadog, Cloudwatch, etc.), we will find a dedicated implementation for MeterRegistry. For this example, we use Prometheus as the monitoring system. In that case, you need to add its Maven dependency:
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
Now, restart the application and check the http://localhost:8080/metrics URL generated by Quarkus Micrometer:
# HELP jvm_threads_live_threads The current number of live threads including both daemon and non-daemon threads
# TYPE jvm_threads_live_threads gauge
jvm_threads_live_threads 64.0
# HELP jvm_memory_max_bytes The maximum amount of memory in bytes that can be used for memory management
# TYPE jvm_memory_max_bytes gauge
jvm_memory_max_bytes{area="nonheap",id="CodeHeap 'profiled nmethods'",} 1.63971072E8
jvm_memory_max_bytes{area="heap",id="G1 Survivor Space",} -1.0
jvm_memory_max_bytes{area="heap",id="G1 Old Gen",} 8.589934592E9
jvm_memory_max_bytes{area="nonheap",id="Metaspace",} -1.0
jvm_memory_max_bytes{area="nonheap",id="CodeHeap 'non-nmethods'",} 7598080.0
jvm_memory_max_bytes{area="heap",id="G1 Eden Space",} -1.0
jvm_memory_max_bytes{area="nonheap",id="Compressed Class Space",} 1.073741824E9
jvm_memory_max_bytes{area="nonheap",id="CodeHeap 'non-profiled nmethods'",} 1.63975168E8
...

These metrics are generated by Micrometer and are compatible with Prometheus. You can try to import them in a standalone Prometheus instance.

First, download Prometheus from https://prometheus.io/download/:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figgy_HTML.png

Choose a suitable edition for your machine/OS and download it. It’s darwin-amd64 in my case.

After decompressing the archive, edit the prometheus-2.27.0.darwin-amd64/prometheus.yml file as follows:
# my global config
global:
  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
...
scrape_configs:
  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
  - job_name: 'prometheus'
    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.
    static_configs:
    - targets: ['localhost:9090']

Change the last line, which defines the targeted /metrics API host location, from localhost:9090 to localhost:8080. This is the application URL.

Run Prometheus:
$ ./prometheus
...msg="Start listening for connections" address=0.0.0.0:9090

You can access it from http://localhost:9090/:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figgz_HTML.png

In the Expression input, enter system_cpu_usage and click Execute. Then click the Graph tab:

../images/509649_1_En_6_Chapter/509649_1_En_6_Figha_HTML.png
Now, you can create your own custom application metrics provider for TokenService:
@Slf4j
@RequestScoped
public class TokenService {
    private static final String TOKENS_REQUESTS_TIMER = "tokensRequestsTimer";     ①
    private static final String TOKENS_REQUESTS_COUNTER = "tokensRequestsCounter";   ②
    @Inject MeterRegistry registry;
    ...
    @PostConstruct
    public void init() {
        registry.timer(TOKENS_REQUESTS_TIMER, Tags.empty());             ①
        registry.counter(TOKENS_REQUESTS_COUNTER, Tags.empty());         ②
    }
    public String getAccessToken(String userName, String password) {
        var timer = registry.timer(TOKENS_REQUESTS_TIMER);               ①
        return timer.record(() -> { var accessToken = "";
            try {
                accessToken = getAccessToken(jwtIssuerUrlProvider.get(),
                                userName, password, clientIdProvider.get(), null);
                registry.counter(TOKENS_REQUESTS_COUNTER).increment();   ②
            } catch (IOException e) { log.error(e.getMessage());
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                log.error("Cannot get the access_token");
            }
            return accessToken;
        });
    }
    ...
}
  • ① Create a timer called tokensRequestsTimer for the duration of executing the getAccessToken() method.

  • ② Create a counter called tokensRequestsCounter, which will increment each time the getAccessToken() method is invoked.

Go to the Swagger UI and request an access_token many times to generate some metrics. Then go back to the Prometheus UI to check the new metrics:

../images/509649_1_En_6_Chapter/509649_1_En_6_Fighb_HTML.png

Take a look at the value reached by the tokensRequestsCounter, based on the previous screenshot. Its Prometheus expression is:

../images/509649_1_En_6_Chapter/509649_1_En_6_Fighc_HTML.png

You can check the Max value for the tokensRequests timer, which matches the tokensRequestsTimer_seconds_max expression in Prometheus:

../images/509649_1_En_6_Chapter/509649_1_En_6_Fighd_HTML.png

Great! ../images/509649_1_En_6_Chapter/509649_1_En_6_Fighe_HTML.gif You have now the basic health check and monitoring components that ensure your application is healthy and has all the required resources.

Conclusion

I consider security and monitoring to be anti-disaster layers, but even with the application of these components, the application still faces many risks. One of them is high availability.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.245.196