Invoke one Microservices from another - PART 3: Propagate Authentication

In this guide, you will learn HOW-TO propagate user authentication and authorization data between two microservices;

Prerequisites

To complete this guide, you need:

Introduction

The scenario consists of:

  • a Service deployed on a Kubernetes cluster which is not exposed outside the cluster

  • a Web Application deployed on a Kubernetes cluster which is exposed outside the cluster and consumes the Service

The user is required to authenticate before using the Web Application and, after authentication happens, we want authentication data to be available, not only to the Web Application, but also tho the Service; authentication is delegated to Keycloak using OpenID Connect protocol;

The overall architecture is:

simple microservice client part3 1

We will start from the two microservices we built in Invoke one Microservices from another - PART 2: Kubernetes and:

  • Microservice A will be the basis for Web Application

  • Microservice B will be the basis for Service

How it works

This is how it works:

  1. The user tries to access Web Application (Microservice A) from a web browser

  2. The browser is redirected to Keycloak where, by providing username and password, the user authenticates itself

  3. The browser is redirected back to Web Application: this time the request contains a JWT Access Token (and a few more tokens actually) provided by Keycloak, containing authentication and authorization data

  4. Web Application validates the JWT Access Token and grants access to the user

  5. Web Application invokes Service (Microservice B) forwarding to it the JWT Access Token it just received

  6. Service validates the JWT Access Token and grants access to Web Application

Keycloak

First, we install and configure Keycloak with users, groups etc..

Run Keycloak on Kubernetes

Create a folder named keycloak wherever you want;

Download file keycloak-realm-realm.json inside the keycloak folder you just created;

Then, create a configmap using it as its content (run the command from the keycloak folder):

kubectl create configmap keycloak-data-import --from-file=keycloak-realm-realm.json=keycloak-realm-realm.json

Create a file named keycloak.yaml in the keycloak folder:

keycloak.yaml:
apiVersion: v1
kind: Service
metadata:
  name: keycloak-external
  labels:
    app: keycloak
spec:
  ports:
    - name: http
      port: 8080
      targetPort: 8080
  selector:
    app: keycloak
  type: NodePort
---
apiVersion: v1
kind: Service
metadata:
  name: keycloak-internal
  labels:
    app: keycloak
spec:
  ports:
    - name: http
      port: 8080
      targetPort: 8080
  selector:
    app: keycloak
  type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: keycloak
  labels:
    app: keycloak
spec:
  replicas: 1
  selector:
    matchLabels:
      app: keycloak
  template:
    metadata:
      labels:
        app: keycloak
    spec:
      volumes:
        - name: keycloak-data-import-volume
          configMap:
            name: keycloak-data-import
      containers:
        - name: keycloak
          image: quay.io/keycloak/keycloak:26.0
          args: [ "start-dev", "--import-realm" ]
          env:
            - name: KEYCLOAK_ADMIN
              value: "admin"
            - name: KEYCLOAK_ADMIN_PASSWORD
              value: "admin"
          ports:
            - name: http
              containerPort: 8080
          readinessProbe:
            httpGet:
              path: /realms/master
              port: 8080
          volumeMounts:
            - name: keycloak-data-import-volume
              mountPath: /opt/keycloak/data/import

Deploy to your Kubernetes Cluster (run the command from the keycloak folder):

kubectl apply -f keycloak.yaml

To access the Keycloak console, find out on what IP address/port, minikube is exposing your keycloak-external service:

$ minikube service keycloak-external --url
http://192.168.39.190:31950

Open the link in your web browser and login to Keycloak with username/password "admin/admin";

Note
since we are using minikube, we expose Keycloak outside the cluster with a NodePort service (keycloak-external) and inside the cluster with a ClusterIP service (keycloak-internal)

optional alternative: configure Keycloak manually

As an alternative to using Keycloak auto import feature (see the "--import-realm" command argument above), you can configure Keycloak manually: remove volumes and volumeMounts and follow these steps:

  1. Create a realm called keycloak-realm

  2. Create a client called simple-microservice-client-secured; in the Capability config, turn on Client authentication.

  3. For the simple-microservice-client-secured client, we also need to set the valid redirect URIs to * and set the Web origins to + to permit all origins of Valid Redirect URIs.

  4. For the simple-microservice-client-secured client, note down the Client Secret in the Credentials tab (e.g. KqIQIzNHD9LnCRjsCxblDnfEl4rcNoKB);

  5. Now, click on Realm roles and create two roles, user and admin.

  6. Create a user called alice and assign her the user and admin roles; set password 123 for alice

  7. Create a user called bob and assign him only the user role; set password 123 for bob

Note
in case you want to go deeper, find more information and examples in Setting up your Keycloak OpenID provider

Web Application (Microservice A)

Maven Project

Copy simple-microservice-client into a new folder named simple-microservice-client-secured;

pom.xml

Update the artifactId to <artifactId>simple-microservice-client-secured</artifactId>;

Add the following to dependencies:

        <dependency>
            <groupId>org.wildfly.security</groupId>
            <artifactId>wildfly-elytron-http-oidc</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <scope>provided</scope>
        </dependency>

Add the following layers in the wildfly-maven-plugin:

    <layer>elytron-oidc-client</layer>

web.xml

Create file src/main/webapp/WEB-INF/web.xml with the following content:

src/main/webapp/WEB-INF/web.xml:
<?xml version="1.0" encoding="UTF-8"?>

<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         metadata-complete="false">
    <security-constraint>
        <web-resource-collection>
            <web-resource-name>secured</web-resource-name>
            <url-pattern>/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>user</role-name>
            <role-name>admin</role-name>
        </auth-constraint>
    </security-constraint>

    <login-config>
        <auth-method>OIDC</auth-method>
    </login-config>

    <security-role>
        <role-name>user</role-name>
    </security-role>
    <security-role>
        <role-name>admin</role-name>
    </security-role>
</web-app>

oidc.json

Create file src/main/webapp/WEB-INF/oidc.json with the following content:

src/main/webapp/WEB-INF/oidc.json:
{
  "client-id" : "simple-microservice-client-secured",
  "provider-url" : "${env.OIDC_PROVIDER_URL:http://localhost:8080}/realms/keycloak-realm",
  "ssl-required" : "EXTERNAL",
  "credentials" : {
    "secret" : "${env.OIDC_CLIENT_SECRET:KqIQIzNHD9LnCRjsCxblDnfEl4rcNoKB}"
  }
}

In case you followed the steps in optional alternative: configure Keycloak manually, replace KqIQIzNHD9LnCRjsCxblDnfEl4rcNoKB with the Client Secret you previously noted down;

Java code

GettingStartedEndpointClient

Add the following interface:

org.wildfly.examples.GettingStartedEndpointClient.java:
package org.wildfly.examples;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

@RegisterClientHeaders
@RegisterRestClient(configKey="simple-microservice-server")
@Path("/hello")
public interface GettingStartedEndpointClient {
	@GET
	@Path("/{name}")
	@Produces(MediaType.TEXT_PLAIN)
	public Response sayHello(@HeaderParam("Authorization") String authorization, final @PathParam("name") String name);
}

GettingStartedEndpoint

Modify the class GettingStartedEndpoint as in the following:

org.wildfly.examples.GettingStartedEndpoint.java:
package org.wildfly.examples;

import jakarta.inject.Inject;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.wildfly.security.http.oidc.OidcSecurityContext;

import java.io.IOException;

@Path("/")
public class GettingStartedEndpoint {

    @Context
    private HttpServletRequest httpServletRequest;

    @Inject
    @RestClient
    private GettingStartedEndpointClient client;

    @GET
    @Path("/{name}")
    @Produces(MediaType.TEXT_PLAIN)
    public Response sayHello(final @PathParam("name") String name) throws IOException {
        Response response;
        OidcSecurityContext oidcSecurityContext = getOidcSecurityContext(httpServletRequest);
        if (oidcSecurityContext != null) {
            String authzHeaderValue = "Bearer " + oidcSecurityContext.getTokenString();
            System.out.println("\n\n[JWT] service Token: " + authzHeaderValue + "\n\n");
            return client.sayHello(authzHeaderValue, name);
        } else {
            System.out.println("\n\n[JWT] No token :(\n\n");
            return client.sayHello(null, name);
        }
    }

    private OidcSecurityContext getOidcSecurityContext(HttpServletRequest req) {
        return (OidcSecurityContext) req.getAttribute(OidcSecurityContext.class.getName());
    }
}

Build and push the image to Quay.io

Build the application:

mvn clean package

Build the Docker image:

podman build -t simple-microservice-client-secured:latest .
Note
You can use wildfly-maven-plugin to automate the image build

Create a public repository named simple-microservice-client-secured on quay.io (e.g. https://quay.io/repository/tborgato/simple-microservice-client-secured).

Note
replace tborgato with the name of your account in all the commands that will follow

Tag the Docker image:

podman tag simple-microservice-client-secured quay.io/tborgato/simple-microservice-client-secured

Push the simple-microservice-client-secured Docker Image:

podman push quay.io/tborgato/simple-microservice-client-secured

Deploy to Kubernetes

Create file simple-microservice-client-secured-deployment.yaml in the kubernetes folder in the root of your project:

simple-microservice-client-secured-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: simple-microservice-client-secured-deployment
  labels:
    app: simple-microservice-client-secured
spec:
  replicas: 1
  selector:
    matchLabels:
      app: simple-microservice-client-secured
  template:
    metadata:
      labels:
        app: simple-microservice-client-secured
    spec:
      containers:
        - name: simple-microservice-client-secured
          image: quay.io/tborgato/simple-microservice-client-secured
          ports:
            - containerPort: 8080
            - containerPort: 9990
          livenessProbe:
            httpGet:
              path: /health/live
              port: 9990
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 9990
          startupProbe:
            httpGet:
              path: /health/started
              port: 9990
          env:
            - name: SIMPLE-MICROSERVICE-SERVER_MP_REST_URI
              value: "http://simple-microservice-server-secured-service:8080"
            - name: OIDC_PROVIDER_URL
              # replace with the outcome of "minikube service keycloak-external --url"
              value: "http://192.168.39.190:31950"
            - name: OIDC_CLIENT_SECRET
              value: "KqIQIzNHD9LnCRjsCxblDnfEl4rcNoKB"

Then:

  • replace "http://192.168.39.190:31950" with the outcome of command minikube service keycloak-external --url (this is where your web browser will be redirected for authentication)

  • replace "tborgato" with your account name on quay.io

  • In case you followed the steps in optional alternative: configure Keycloak manually, replace KqIQIzNHD9LnCRjsCxblDnfEl4rcNoKB with the Client Secret you previously noted down;

Deploy to your Kubernetes Cluster:

kubectl apply -f kubernetes/simple-microservice-client-secured-deployment.yaml

Create file simple-microservice-client-secured-service.yaml in the kubernetes folder:

simple-microservice-client-secured-service.yaml:
apiVersion: v1
kind: Service
metadata:
  name: simple-microservice-client-secured-service
  labels:
    app: simple-microservice-client-secured
spec:
  ports:
    - name: http
      protocol: TCP
      port: 8080
      targetPort: 8080
  selector:
    app: simple-microservice-client-secured
  type: NodePort

Deploy to your Kubernetes Cluster:

kubectl apply -f kubernetes/simple-microservice-client-secured-service.yaml

Service (Microservice B)

Maven Project

Copy simple-microservice-server into a new folder named simple-microservice-server-secured;

pom.xml

Update the artifactId to <artifactId>simple-microservice-server-secured</artifactId>;

Add the following to dependencyManagement:

<dependency>
    <groupId>org.wildfly.bom</groupId>
    <artifactId>wildfly-expansion</artifactId>
    <version>${version.wildfly.bom}</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

Add the following to dependencies:

        <dependency>
            <groupId>org.eclipse.microprofile.config</groupId>
            <artifactId>microprofile-config-api</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.microprofile.jwt</groupId>
            <artifactId>microprofile-jwt-auth-api</artifactId>
            <scope>provided</scope>
        </dependency>

Add the following layers in the wildfly-maven-plugin:

        <layer>microprofile-config</layer>
        <layer>microprofile-jwt</layer>

microprofile-config.properties

Add file src/main/resources/META-INF/microprofile-config.properties with the following content:

microprofile-config.properties:
mp.jwt.verify.publickey.location=http://localhost:8080/realms/keycloak-realm/protocol/openid-connect/certs

Java code

GettingStartedApplication

Modify the class GettingStartedApplication as in the following:

org.wildfly.examples.GettingStartedApplication.java:
package org.wildfly.examples;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;
import org.eclipse.microprofile.auth.LoginConfig;

@LoginConfig(authMethod="MP-JWT")
@ApplicationPath("/hello")
public class GettingStartedApplication extends Application {

}

GettingStartedEndpoint

Modify the class GettingStartedEndpoint as in the following:

org.wildfly.examples.GettingStartedEndpoint.java:
package org.wildfly.examples;

import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.jwt.JsonWebToken;

@Path("/")
public class GettingStartedEndpoint {

	@Inject
	@ConfigProperty(name = "mp.jwt.verify.publickey.location")
	private String publicKeyLocation;

	@Inject
	JsonWebToken jwt;

	@GET
	@Path("/{name}")
	@Produces(MediaType.TEXT_PLAIN)
	@RolesAllowed({"admin"})
	public Response sayHello(final @PathParam("name") String name) {
		System.out.println("mp.jwt.verify.publickey.location=" + publicKeyLocation);

		String response =
				"Hello " + name
						+ (jwt != null ? (" Subject:" + jwt.getSubject()) : null)
						+ (jwt != null ? (" Issuer: " + jwt.getIssuer()) : null);

		return Response.ok(response).build();
	}
}

Build and push the image to Quay.io

Build the application:

mvn clean package

Build the Docker image:

podman build -t simple-microservice-server-secured:latest .

Create a public repository named simple-microservice-server-secured on quay.io (e.g. https://quay.io/repository/tborgato/simple-microservice-server-secured).

Note
replace tborgato with the name of your account in all the commands that will follow

Tag the Docker image:

podman tag simple-microservice-server-secured quay.io/tborgato/simple-microservice-server-secured

Push the simple-microservice-server-secured Docker Image:

podman push quay.io/tborgato/simple-microservice-server-secured

Deploy to Kubernetes

Create file simple-microservice-server-secured-deployment.yaml in the kubernetes folder:

simple-microservice-server-secured-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
  name: simple-microservice-server-secured-deployment
  labels:
    app: simple-microservice-server-secured
spec:
  replicas: 1
  selector:
    matchLabels:
      app: simple-microservice-server-secured
  template:
    metadata:
      labels:
        app: simple-microservice-server-secured
    spec:
      containers:
        - name: simple-microservice-server-secured
          image: quay.io/tborgato/simple-microservice-server-secured
          ports:
            - containerPort: 8080
            - containerPort: 9990
          livenessProbe:
            httpGet:
              path: /health/live
              port: 9990
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 9990
          startupProbe:
            httpGet:
              path: /health/started
              port: 9990
          env:
            - name: MP_JWT_VERIFY_PUBLICKEY_LOCATION
              value: "http://keycloak-internal:8080/realms/keycloak-realm/protocol/openid-connect/certs"

Then:

  • replace "tborgato" with your account name on quay.io

Deploy to your Kubernetes Cluster:

kubectl apply -f kubernetes/simple-microservice-server-secured-deployment.yaml

Create file simple-microservice-server-secured-service.yaml in the kubernetes folder:

simple-microservice-server-secured-service.yaml:
apiVersion: v1
kind: Service
metadata:
  name: simple-microservice-server-secured-service
  labels:
    app: simple-microservice-server-secured
spec:
  ports:
    - name: http
      protocol: TCP
      port: 8080
      targetPort: 8080
  selector:
    app: simple-microservice-server-secured
  type: ClusterIP

Deploy to your Kubernetes Cluster:

kubectl apply -f kubernetes/simple-microservice-server-secured-service.yaml

Test

$ minikube service simple-microservice-client-secured-service --url
http://192.168.39.190:32225

Open that URL in your browser, log in as alice/123 and try it out!

After pressing the "Say Hello" button, you should see something like:

Hello ddd Subject:aaef43ee-4005-4d2d-a5f0-0e0d11a1f831 Issuer: http://192.168.39.190:31950/realms/keycloak-realm
< Back to Guides