How to create a Keycloak authenticator as a microservice?

We have been using Keycloak for some time now, for various types of projects and several use-cases. It provides great flexibility and precision in the world of CIAM. Every limitation that you find is often already discussed by the community. I now want to share back to the community a construction for solving a limitation that I faced.

The problem: While Keycloak has a very powerful extension mechanism with the Service Provider Interface (SPI), it implies strong understanding of Keycloak class model, the need to be java fluent and an in-process deployment of your custom code that could potentially break the entire server in case of bug. We were developing backend in NodeJs javascript/typescript with a microservice approach and all our tooling was designed for that stack therefore we were not ready to add a java stack.

However we needed to enable Keycloak to handle custom authentication such as 2FA, Where-are-you-from, password migration…

The approach to solving the problem: Keycloak offers a script authenticator, still in preview with version 10, that enables javascript (Nashorn) processing using its basic class model. However, such approach makes it difficult to interact with external systems or to have complex business logic. However, and this is the trick, it can be used to make http request and act upon the response. We therefore constructed an authenticator wrapper, logic agnostic, which expect the logic to be implemented in a REST microservice.

Now, our business logic to perform custom authentication can be done with the same stack as the rest of our work leveraging the team experience and isolating the custom code from Keycloak specifics.

How to use the open source version?

The solution is packaged into a single NPM package keycloak-rest-authenticator that is designed to be used with express js framework.

Note that in this article I will demonstrate the code with javascript, but you can very well do it with typescript.

1Create the express service (you know how to do this so I’ll be quick): The code is javascript

// index.js
const express = require(“express”);
const app = express();
app.use(express.json());
app.get(“/”, (req, res)=>{ res.json({hello: “it works”}) })app.listen(8080, ()=>{
console.log(“Listening on port 8080”)
})

2Import the package and use it:

npm i --save keycloak-rest-authenticator

Add the reference in index.js (note that types are included) and declare the endpoint /myauth

// index.js
const express = require("express");
const {KeycloakScriptPackager, RestAuthenticator} =
require("keycloak-rest-authenticator").default;
const app = express();
app.use(express.json());
const packager = new KeycloakScriptPackager(
{keycloakAccessibleBaseUrl:"http://localhost:8080"}
);
app.use(RestAuthenticator.declare(
"/myauth",
new MyAuthenticator(),
// will be defined below
packager
));
app.listen(8080, ()=>{
console.log("Listening on port 8080")
})
  • The packager is an object that will enable easy deployment to keycloak of the script authenticator. We will discuss below the configuration.
  • RestAuthenticator.declare() associates the /myauth endpoint with the implementation MyAuthenticator and tells the packager to add it for deployment.

3Implement your authenticator:

Create a new javascript file my-authenticator.js to implement the authenticator business logic. Say we want the user to have an attribute with a particular value to be authorised to connect (not really meaningful but simple enough to start with)

// my-authenticator.js (pure javascript implementation)
const {createAuthenticator} = require("keycloak-rest-authenticator")
const MyAuthenticator = createAuthenticator(function() {
this.expectedUserAttribute = "MyAttribute";
this.expectedUserAttributeValue = "TestedValue";
console.log("Creating the instance");
}, {
processNew: async function (req) {
if(req.user &&
req.user.attributes[this.expectedUserAttribute] ==
this.expectedUserAttributeValue) {
return {} // meaning successful verification
} else {
const createUserAttributes={};
createUserAttributes[this.expectedUserAttribute] =
"not set yet"
return {
userAttributes: createUserAttributes,
failure: "invalidCredentials"
}
}
},
})
module.exports = MyAuthenticator;

A valid authenticator, is a class that creates object with three functions: processNew for the first call on the authenticator, processInteraction for subsequent calls on the same authentication process, and possibly processInterruption for handling concurrent calls on the same process.

The createAuthenticator function is useful in javascript to be guided by the intellisense of your editor to create an implementation of the KeycloakRestAuthenticator interface you would normally implement in typescript. It takes a first parameter as a constructor function, that will become the class. the second parameter are the functions to implement the interface.

The equivalent in typescript would be:

// my-authenticator.ts (typescript implementation)import { 
KeycloakRestAuthenticator,
AuthenticationRequest,
AuthenticationResponse} from "keycloak-rest-authenticator"
export class MyAuthenticator implements KeycloakRestAuthenticator {
expectedUserAttribute: string;
expectedUserAttributeValue: string;
constructor() {
this.expectedUserAttribute = "MyAttribute";
this.expectedUserAttributeValue = "TestedValue";
}
async processNew(req: AuthenticationRequest):
Promise<AuthenticationResponse | null> {
if(req.user &&
req.user.attributes[this.expectedUserAttribute] ==
this.expectedUserAttributeValue) {
return {} // meaning successful verification
} else {
const createUserAttributes={};
createUserAttributes[this.expectedUserAttribute] =
"not set yet"
return {
userAttributes: createUserAttributes,
failure: "invalidCredentials"
}
}
}
async processInteraction(req: AuthenticationRequest):
Promise<AuthenticationResponse | null> {
throw new Error("Method not implemented.");
}
async processInterruption(req: AuthenticationRequest):
Promise<AuthenticationResponse | null> {
throw new Error("Method not implemented.");
}
}

The processNew function, here, is an async function that

  • returns a successful authentication if the user in keycloak has the “MyAttribute” attribute set to the value “TestedValue”
  • returns a failed authentication if the attribute is different. Then initialise that attribute to “not set yet”
// modify index.js
// add the declaration of the authenticator implementation
const MyAuthenticator = require("./my-authenticator.js")
...
app.use(RestAuthenticator.declare(
"/myauth",
new MyAuthenticator(),
packager
));

4Configure the deployment

The KeycloakScriptPackager instance must be initialised with the configuration that will enable the node service to deploy its definition into keycloak environment and for keycloak server to reach the service endpoint.

Keycloak auto-deploy jar files (containing the definition of the authenticators) when these jar are placed in a folder located at keycloak/standalone/deployments. So, the configuration accepts a keycloakDeploymentLocation field with the path to that folder.

Keycloak then needs to be told the absolute url where to reach the node service. For this the configuration must be given the keycloakAccessibleBaseUrl field.

const packager = new KeycloakScriptPackager({
keycloakDeploymentLocation:
"/opt/jboss/keycloak/standalone/deployments",
keycloakAccessibleBaseUrl: "http://host.docker.internal:8080",
});

Then, you need to tell the node service to create and deploy the jar file to keycloak. The make() function of the packager does this and place the created jar in the keycloak designated folder. It requires that the jar shell command exists and is reachable in the PATH, otherwise, it can be configure, see the docs for more details.

app.listen(8080, ()=>{
console.log("Listening on port 8080");
packager.make();
})

Before going further, ensure that your Keycloak instance has the script feature enabled. Refer to this documentation to set it up.

Run the node service as a normal nodejs process(node index.js ). You can activate the DEBUG environment variable as DEBUG=rest-authenticator:* to see the processing output.

Connect to the admin console of your keycloak instance and go to the Server Info / Providers page and check that you see a “script-/myauth/authenticator-rest.js” in the authenticator section. If not check the logs both on your service and keycloak. Check also that the jar file has been created.

5Add the authenticator to the login flow and test

It is now time to configure keycloak so the deployed authenticator can be used.

  • Go to the keycloak admin console, select the realm where you need that authentication and reach the Authentication page.
The default Browser authentication flow
  • Make a copy of the of the flow to be able to modify it : “Copy of Browser”
  • Add an Execution in the “Copy of Browser Forms”, and in the selection page find the “http://localhost:8080/myauth” authenticator. Click save
  • Make the execution “Required” so it will be activated at time of login
modified Browser flow with our new authenticator
  • In the flow binding of the realm, select the new login flow as follow and save.
  • Try to authenticate to the realm with an existing user and even with the right password it should now fail because the user does not have the correct attribute set.
  • Now go to the user configuration and open the attributes page. The new attribute will be there with the value “not set yet”.

Set the attribute value to “TestedValue” and try login again. This time it works.

Well done you have deployed your first microservice authenticator for Keycloak.

Let’s dig into the solution

The solution is made of one layer in each process to ease the integration:

  1. In keycloak is hosted a generic script (the same for all endpoint) that collect all the context information, create a json and call the rest endpoint. With the json response, process according to the fields set.
  2. In the NodeJs process a middleware performs the json interpretation and calls the appropriate implementation function. Then marshal the response back to keycloak.

In more details this is what is happening:

Yes, it looks complex … But the complexity gets hidden for you. You should concentrate your efforts in what is happening in the UserAgent layer (the end-user browser) and what needs to be the behaviour in the Authenticator Impl layer.

In Keycloak process, the Rest Authenticator layer is the actual Nashorn javascript implementation. You can find the code here. It is this piece of code that is responsible for interacting with Keycloak data model and submitting the request to the NodeJs service. Keep in mind that keycloak is multi-threaded so the http call is blocking for the thread.

In the NodeJs process, the RestAuthenticator layer is an express middleware responsible for few controls (validating the json structure) and directing to the right implementation function. You can find the code here. It also protects the service from unexpected exceptions. Keep in mind that NodeJs is mono-threaded, so if you have high volume and work intense processing, multiply the number of instances behind a load balancer and keep your implementation stateless.

The above diagram is explained in the Readme of the repo.

I hope you will find this approach useful and please in any case give me some feedbacks so it can be improved further.

Issues can be posted at https://gitlab.com/guenoledc-perso/keycloak/rest-authenticator/-/issues.

If you want to see an end to end integration visit the videos showing the result of authenticating users with Duo Mobile integrated with the backend api.

--

--

--

Results of personal work around OIDC, Keycloak mostly in NodeJs / Typescript

Recommended from Medium

Moving an existing project to Blitzjs

Variable Declaration in Javascript for Beginners

Basic text processing and data extraction in Linux

How to use render method to React

Render Method

Test Driven Development Will Drive You Back to Basics

A child is driving a miniature toy car.

WordPress Gutenberg — Adapting Shortcodes to Work as Blocks

Introducing StateAdapt: reusable, reactive state management

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Guénolé de Cadoudal

Guénolé de Cadoudal

Developper passionated with Blockchain. Currently Digital Factory Officer for CACIB.

More from Medium

Microservices API Gateway vs. Traditional API Gateway

Setting up a NodeJS API with TypeScript: Part 2

An Introduction to MQTT with NestJS Framework

Adding dependencies to your Node.js projects