Authorizing multi-language microservices with Louketo Proxy

Update August 25, 2020: The Louketo Proxy team has announced that it is sunsetting the Louketo project. Read the link for more information, and watch our site for a new article detailing how to authorize multi-language microservices using a different method.

What if you needed to provide authentication to several microservices that were written in different languages? You could use Red Hat Single Sign-On (SSO) to handle the authentication, but then you would still need to integrate each microservice with Keycloak. Wouldn't it be great if a service could just handle the authentication flow and pass the user's details directly to your microservices? In this article, I introduce a service that does just that!

Louketo Proxy

Louketo Proxy (previously Keycloak-Gatekeeper) integrates with OpenID Connect (OIDC)-compliant providers like Keycloak. Louketo Proxy hands off the authentication to Keycloak, and then passes the authorization and user details to a microservice as header attributes. The diagram in Figure 1 illustrates the authentication flow between Louketo Proxy, Keycloak, and a microservice.

A diagram of Louketo Proxy's authentication flow.
Figure 1: Louketo Proxy authenticates a microservice with Keycloak.

With Louketo Proxy, you don't need to worry about supporting the authentication flow across the different languages used for your microservices. As an added benefit, Louketo Proxy makes it easy to provide authentication to legacy off-the-shelf applications that do not support OIDC.

Louketo Proxy in OpenShift

Before getting started, it is worthwhile to explore the architecture that is required to run Louketo Proxy and a microservice together. In a Red Hat OpenShift deployment, the usual pattern is for a single pod to run a single container, as shown in Figure 2.

A diagram of an OpenShift deployment where one pod runs one container.
Figure 2: A typical OpenShift deployment where one pod runs one container.

For our Louketo Proxy pattern to work, we need one pod to run two containers. Louketo Proxy and the microservice will reside within the same pod and share the resources associated with that pod, including networking. To prevent conflict, Louketo Proxy and the microservice will need to listen on different network ports. The Louketo Proxy instance also must be able to communicate with the microservice without traversing any network links. This setup reduces latency and makes it difficult to execute a man-in-the-middle (MITM) attack, which improves the security model. Figure 3 shows the OpenShift deployment pattern for Louketo Proxy with microservices.

Authorizing multi-language microservices with Louketo Proxy
Figure 3: The Louketo Proxy instance and microservice run in the same pod and share the pod's resources.

Normally, we would create a service bound to the microservice and expose it via a route. In this case, we only create a service bound to the Louketo Proxy instance and expose it via a route. Consumers will only ever access the microservice via the Louketo Proxy instance. This arrangement enforces the security model.

Getting started with Louketo Proxy

For this example, we deploy a simple Flask application that exposes the authentication headers passed along by Louketo Proxy. If you wish, you can run the example code on Red Hat CodeReady Containers (CRC). Otherwise, you will need to update the code to run on your OpenShift cluster.

Step 1: Deploy Keycloak

The first step is to deploy Keycloak on CodeReady Containers. To fast-track the process, run the following commands from a terminal. These will create a project on OpenShift, deploy Keycloak, and create a route:

$ oc new-project sso-test
$ oc new-app --name sso --docker-image=quay.io/keycloak/keycloak -e KEYCLOAK_USER='admin' -e KEYCLOAK_PASSWORD='louketo-demo' -e PROXY_ADDRESS_FORWARDING='true’
$ oc create route edge --service=sso --hostname=sso.apps-crc.testing

Create and configure the Flask client

Browse to https://sso.apps-crc.testing and log in with the username admin and the password louketo-demo. Once there, select Clients from the left-hand menu and create a new client with the fields shown in Figure 4.

A screenshot of the Add Client dialog.
Figure 4: Add a new Flask client.

After you have created the client, you will have the option to switch the client access type from public to confidential, as shown in Figure 5.

A screenshot of the Settings dialog in the Flask UI.
Figure 5: Use the Settings dialog to switch client access to confidential.

CodeReady Containers will generate a client secret, which you can view under the Credentials tab shown in Figure 6. You will need the secret when we configure Louketo Proxy, so make a note of it.

A screenshot of the Credentials dialog in the Flask UI.
Figure 6: Check under the Credentials tab to see the generated client secret.

Configure the mappers

At this point, we are still configuring the Flask client. Select the Mappers tab and add two mappers:

  • Groups:
    • Name: groups
    • Mapper type: Group Membership
    • Token claim name: groups
  • Audience:
    • Name: audience
    • Mapper type: Audience
    • Included client audience: flask

Figure 7 shows the configuration for the Audience mapper.

A screenshot of the dialog to create the Audience mapper.
Figure 7: Configure the Flask client's Audience mapper.

Figure 8 shows the configuration for the Groups mapper.

A screenshot of the dialog to create the Groups mapper.
Figure 8: Configure the Flask client's group-membership mapper.

Configure the user groups

Now select Groups from the left-hand menu and add two groups:

  • admin
  • basic_user

Configure the user

Again from the left-hand menu, select Users and add a user. Be sure to enter an email and set a password for the user, then add the user to the basic_user and admin groups that you just created. Next, we'll configure Louketo Proxy and the example application.

Step 2: Configure Louketo Proxy and the application

We will need the following details from our Keycloak server (note that you will enter the client secret that you saved in Step 1):

  • Client ID: flask
  • Client secret
  • Discovery URL: https://sso.apps-crc.testing/auth/realms/master
  • Groups: basic_user

Create the ConfigMaps

First, we'll use a ConfigMap to configure Louketo Proxy. Louketo Proxy supports presenting custom pages (sign-in pages, forbidden pages, and so on) that can match the look-and-feel of the associated microservice. For our use case, we will redirect all unauthenticated requests to Keycloak. Make sure that you replace the client-secret in the following ConfigMap with the one generated by Keycloak:

apiVersion: v1
kind: ConfigMap
metadata:
 name: gatekeeper-config
data:
 keycloak-gatekeeper.conf: |+
   # The URL for retrieving the OpenID configuration - normally the /auth/realms/
   discovery-url: https://sso.apps-crc.testing/auth/realms/master
   # skip tls verify
   skip-openid-provider-tls-verify: true
   # the client ID for the 'client' application
   client-id: flask
   # the secret associated with the 'client' application
   client-secret: < Paste Client Secret here >
   # the interface definition you wish the proxy to listen to, all interfaces are specified as ':', unix sockets as unix://|
   listen: :3000
   # whether to enable refresh-tokens
   enable-refresh-tokens: true
   # the location of a certificate you wish the proxy to use for TLS support
   tls-cert:
   # the location of a private key for TLS
   tls-private-key:
   # the redirection URL, essentially the site url, note: /oauth/callback is added at the end
   redirection-url: https://flask.apps-crc.testing
   secure-cookie: false
   # the encryption key used to encode the session state
   encryption-key: nkOfcT6jYCsXFuV5YRkt3OvY9dy1c0ck
   # the upstream endpoint which we should proxy request
   upstream-url: http://127.0.0.1:8080/
   resources:
   - uri: /*
     groups:
     - basic_user

Next, we'll create a ConfigMap for the Flask application. Before we can create the ConfigMap, we need to obtain the public key for Keycloak. To get the public key, log in to the Keycloak admin page and go to Realm Settings > Keys. From there, select the RS256 public key, as shown in Figure 9.

A screenshot of the option to select the RSA-generated public key.
Figure 9: Select the RSA-generated public key.

The Flask application uses this public key to check the validity of the JSON Web Token (JWT) that Louketo Proxy passes. Here is the ConfigMap for the Flask application:

kind: ConfigMap
apiVersion: v1
metadata:
 name: sso-public-key
data:
 PUBLIC_KEY: |
   -----BEGIN PUBLIC KEY-----
   < Paste in the Keycloak public key here >
   -----END PUBLIC KEY-----

Bind the service

Now we need to bind a service to the Louketo Proxy listening port. We defined the listening port in Louketo Proxy's ConfigMap under the listen parameter. Here is the service binding:

apiVersion: v1
kind: Service
metadata:
 name: flask
spec:
 ports:
 - port: 3000
   protocol: TCP
   targetPort: 3000
 selector:
   app: flask

Configure the route

We use routes to expose our services to consumers. In this case, we are exposing the Louketo Proxy service and the terminating TLS on the router:

kind: Route
apiVersion: route.openshift.io/v1
metadata:
 name: flask
spec:
 host: flask.apps-crc.testing
 to:
   kind: Service
   name: flask
   weight: 100
 port:
   targetPort: 3000
 tls:
   termination: edge
   insecureEdgeTerminationPolicy: Redirect
 wildcardPolicy: None

Apply the DeploymentConfig

Now that we have created the ConfigMaps, service, and route we can apply our OpenShift DeploymentConfig. If you look carefully at the configuration below, you will see that we are deploying two containers within the same DeploymentConfig. As I said at the beginning of this article, our architecture calls for deploying two containers within the same pod:

kind: DeploymentConfig
apiVersion: apps.openshift.io/v1
metadata:
 name: flask
 labels:
   app: flask
spec:
 strategy:
   type: Rolling
 replicas: 1
 selector:
   app: flask
 template:
   metadata:
     labels:
       app: flask
   spec:
     containers:
       - name: flask
         image: quay.io/rarm_sa/flask-sso-gatekeeper
         ports:
           - containerPort: 8080
             protocol: TCP
         envFrom:
           - configMapRef:
               name: sso-public-key
         imagePullPolicy: IfNotPresent
       - name: gatekeeper
         image: 'quay.io/louketo/louketo-proxy'
         args:
           - --config=/etc/keycloak-gatekeeper.conf
         ports:
           - containerPort: 3000
             name: gatekeeper
         volumeMounts:
           - name: gatekeeper-config
             mountPath: /etc/keycloak-gatekeeper.conf
             subPath: keycloak-gatekeeper.conf
     volumes:
       - name : gatekeeper-config
         configMap:
           name: gatekeeper-config

Test the configuration

To test your configuration, browse to the example application at https://flask.apps-crc.testing. You will be redirected to Keycloak and presented with a login screen like the one in Figure 10. Enter the username and password for your application user.

A screenshot of the Keycloak login screen.
Figure 10: The Keycloak login screen.

Once you have authenticated the user, you will be redirected to an application page that returns a JSON file. The file exposes the headers passed along by Louketo Proxy, as shown in Figure 11.

A screenshot of the JSON file.
Figure 11: The Flask application returns a JSON file that exposes the Louketo Proxy headers.

Conclusion

This article introduced you to Louketo Proxy, a simple way to provide authentication for your applications without having to code your own OpenID Connect clients within your microservices. As always, I welcome your questions and feedback in the comments.

Last updated: August 25, 2020