Software project: Creating a real-life transactional microservices application on Kubernetes

Mika Rinne, Oracle EMEA
7 min readJan 23, 2024

--

Part 6: Adding the UI and security

Now that I have the Gymapp microservices REST API’s done in Part 5 let’s add the UI for the app.

I decided to use VueJS with Bootstrap 5 to create the responsive single page web app UI’s for it in both microservices, Gyminstructor and Gymuser.

Adding this to the Gymapp Helidon 4 project is easy. First added the following configuration for the static pages to the Helidon starter generated src/main/resources/META-INF/microprofile-config.properties file:

server.static.classpath.location=/WEB
server.static.classpath.welcome=index.html

Then the html source with VueJS and boostrap 5 libs can be saved to src/main/resources/WEB/index.html file.

SPA UI in src/main/resources/WEB/index.html of the Gymuser microservice

Editing the html source to create the desired result is nice in Helidon Dev since saving the page will recompile the project automatically to see and test the results immediately. Here’s how the UIs look like:

Gymuser responsive UI
Gyminstructor responsive UI

Here’s a silent Youtube video of the app in action in Dev (not latest version but will demonstrate the functionality) — watch with the titles on:

Editing the html source in VS Code to create the desired result is nice in Helidon Dev since saving the page will recompile the project automatically to see and test the results immediately.

To protect the Gymapp REST APIs and authenticate and authorize (A&A) the users I had the following design considerations in mind:

  • Secure API endpoints and UIs using JWT
  • Users can access only their own data (except in a few occasions)
  • Minimize development of custom UIs and APIs, also for the user management
  • Gymusers can self-register
  • Compliant with MicroTX and Istio service mesh in the Kubernetes environment

Hence I decided to go with Oracle IDCS as part of the Oracle Cloud Infrastructure (OCI).

To do this I did the following for my Dev:

  • Created 2 confidental apps in IDCS using OCI Cloud UI, one for Gymuser Dev and one for Gyminstructor Dev
  • Configure the confidental app in Helidon security config per Microservice
  • Created a custom login scheme using JS in the index.hml of both microservices and IDCS URLs to redirect from my localhost to IDCS and back during A&A
  • I did not implement a custom login page to IDCS, however, and use the IDCS default one (as seen below)

Here is how my IDCS confidental app config looks per microservice in Dev in src/main/resources/application.yaml, naturally the parameters differ a bit of each other and this is for Gymuser microservice running in port 8080 and Gyminstructor in port 8081:

security:
config.require-encryption: false
properties:
# This is a nice way to be able to override this with local properties or env-vars
idcs-uri: "https://idcs-b491...1665.identity.oraclecloud.com:443"
idcs-client-id: "2835...24cbe"
idcs-client-secret: "${CLEAR=9f7a7...d99e}"
proxy-host: ""
providers:
- abac:
# Adds ABAC Provider - it does not require any configuration
- oidc:
client-id: "${security.properties.idcs-client-id}"
client-secret: "${security.properties.idcs-client-secret}"
identity-uri: "${security.properties.idcs-uri}"
# This redirect URI which should match at IDCS registered application Redirect URL
# Redirect URL at IDCS follows http://<hostname:8080 or Load Balancer>/oidc/redirect
redirect-uri: "/index.html"
# scope-audience should match with IDCS Primary Audience , except adding "/" trailing character.
# At IDCS it will be http://<hostname:8080 or Load Balancer>/<REST endpoint>/.
# Mismatch in scope-audience causes failure in generating access token
scope-audience: "http://gymuser:8080"
# Mismatch in audience causes failure in generating access token
audience: "${security.properties.idcs-uri}"
# Front end host , it should be either hostname:8080 or load balancer ip
frontend-uri: "http://gymuser:8080"
server-type: "idcs"
logout-enabled: false
propagate: false
redirect: true
cookie-use: false
header-use: true

Using this config the IDCS will provide the login page URL to the Gymuser microservice when the REST APIs cannot A&A the Gymuser using JWT. In that case the UI will provide a sign-in and sign-up -popup (Gyminstructor will just do the redirect to the IDCS login instead since self-registering is not enabled):

Gymapp UI Sign-in and Sign-up selection popup

If user chooses sign-in the Gymapp will redirect the user to the IDCS login page of the confidental app used (in the other case it will redirect to IDCS sign-up page):

IDCS default sign-in (login) screen that Gymapp uses

A successful login will redirect back to the Gymuser index.html and the Security Helidon config is able to authenticate the user with the Authenticate annotation in the Gymuser REST APIs as it was already shown in the code examples in Part 5.

Using the JWT and the IDCS callback URL here is how I do the login for the Gymuser in the REST API (http POST) below. Using the JWT principal it will create the user in-flight if he/she does not exist in local database allowing self-registration as Gymuser:

@Authenticated
@POST
@Path("/login")
@Transactional(Transactional.TxType.REQUIRED)
@Produces(MediaType.APPLICATION_JSON)
public Gymuser getGymuserByToken(@Context SecurityContext context) {
try {
Optional<Principal> userPrincipal = context.userPrincipal();
String userEmail = userPrincipal.get().getName();
Optional<Object> obj = userPrincipal.get().abacAttribute("user_displayname");
String userName = obj.get().toString();
TypedQuery<Gymuser> query = entityManager.createNamedQuery("getGymuser", Gymuser.class);
List<Gymuser> list = query.setParameter("email", userEmail).getResultList();
Gymuser gymuser = null;
if (list.isEmpty()) {
gymuser = new Gymuser();
gymuser.setEmail(userEmail);
gymuser.setName(userName);
entityManager.persist(gymuser);
} else {
gymuser = list.get(0);
}
return gymuser;
} catch (Exception e)
{
e.printStackTrace();
throw new BadRequestException("Unable to create gymuser");
}
}

And here is the respective JavaScript on the login.html to make the above REST API call:

... // Not complete JS ...
axiosInstance
.post('/gym/login/', '')
.then(response =>
{
// Login was succesful, setup the user on page
data.gymuser = response.data;
getMyclasses(); // Get Myclasses onto the page
getInstructorclasses(); // Get Instructorclasses onto the page
}
).catch(error => {
// Redirect to IDCS login page
data.gymuser = {};
data.idcsUrl = error.request.responseURL;
data.showLogin = 1;
})
... // Not complete JS ...

The Gymapp REST API authorization in Dev is simple. I have created two user groups, one per confidental app/microsevice and only the users in this group can access all REST API’s in the respective app. Hence, no annotations are used for this in code.

However, the IDCS scenario changes a bit later when moving to the Kubernetes environment and using the Istio service mesh that is part of the Kubernetes MicroTX install. I will cover this in detail in later part, but in short using only single IDCS (that is having the same IDCS config for both/all microservices) the JWT is shared across microservices and additional authorization is required for Gyminstructor REST APIs to prevent Gymusers from accessing these.

To do this I have created a custom method to check whether the logged in user belongs to Gymistructors group using the IDCS Asserter REST API. Unless belonging to this group the end user cannot access the Gyminstructor REST APIs. Hence also the login to the Gyminstructor UI fails. (Alternatively I could be using IDCS role mapping and respective annotations in Helidon but I need to do some further testing on this).

Here’s the code below to call the asserter method (“isInstructor(userEmail)”) in one of the Gyminstructor REST APIs I covered earlier in Part 5 (this method call was omitted in the code below).

@Authenticated
@POST
@Path("/complete/{id}")
@Produces(MediaType.APPLICATION_JSON)
@Transactional(Transactional.TxType.REQUIRED)
public Response completeInstructorclass(@Context SecurityContext context,
@HeaderParam("Authorization") String auth,
@PathParam("id") Long id) {
Optional<Principal> userPrincipal = context.userPrincipal();
String userEmail = userPrincipal.get().getName();
if(!isInstructor(userEmail))
{
return Response.serverError().build();
}
// Method continues ...

This image describes the situation in Kubernetes using the Istio service mesh and the custom IDCS assertion:

Microservices authorization using custom IDCS assertion in Istio service mesh on Kubernetes

I’ve modified the Gymapp UI for Istio so that the JWT will be copied and shared accross and login needs to happen only once when using both UIs.

There are few use cases where I’m not using the Helidon authentication with the annotation, both having inter-microservice calls:

  • Gymuser accessing (listing) available Gyminstructor classes
  • Gymuser attending to a class and making a sign-up to Gyminstructor

Basically I just custom validate the JWT. This is not to block the access between microservices in those cases and prevent any unwanted interruptions during the signup process to class with a PayPal® payment that could happen abruptly in case of a JWT session timeout for instance.

In Part 7 I will build and deploy the Gymapp to OCI Kubernetes (OKE) with VS Code OCI DevOps extensions and cover the MicroTX install for it.

--

--