Accessing Keycloak API using Node.js and AWS Lambda — Part 1

Jawad Rashid
13 min readJul 29, 2024

--

Introduction

In this article I will use the docker image I created in the following tutorial https://medium.com/@jawadrashid/implementing-keycloak-event-listener-spi-service-provider-interfaces-1f01ae819e8d and enhance it to use AWS lambda to interact with this keycloak server using lambda. I will write Java and NodeJS versions of my code in AWS lambda to:

  1. Get all active sessions for a user
  2. Terminate all active sessions for an agent

This is part 1 discussing how to write code in Node.JS. In next part we will discuss using Java code.

Table Of Contents

Docker Setup

Here is the docker-compose.yaml for this app. There are many differences from the last tutorial because there are containers for running lambda code in nodeJs. I will explain the lambda code shortly but here is docker-compose.yaml in root of the setup.

services:
keycloak_demo:
image: quay.io/keycloak/keycloak:25.0.2
command: start-dev
environment:
KC_DB: postgres
KC_DB_URL_HOST: postgres_keycloak_demo
KC_DB_URL_DATABASE: keycloak
KC_DB_PASSWORD: password
KC_DB_USERNAME: keycloak
KC_DB_SCHEMA: public
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
ports:
- "8890:8080"
depends_on:
postgres_keycloak_demo:
condition: service_healthy
networks:
- keycloak_demo_dev_network

postgres_keycloak_demo:
image: postgres:16.3
command: postgres -c "max_connections=200"
volumes:
- pgdata_keycloak_data:/var/lib/postgresql/data
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: password
healthcheck:
test: "exit 0"
ports:
- "5436:5432"
networks:
- keycloak_demo_dev_network

lambda-node:
build: ./lambda-node
ports:
- "9000:8080"
depends_on:
- keycloak_demo
networks:
- keycloak_demo_dev_network


volumes:
pgdata_keycloak_data:
networks:
keycloak_demo_dev_network:
driver: bridge

Let me discuss some things about the docker file. There are 3 services one for keycloak named keycloak_demo, another for postgresql named postgres_keycloak_demo, and lastly one service for Node.JS lambda which I will explain later.

The configuration for Keycloak is very standard we are defining the Postgresql credentials for Keycloak to connect to, also we have set up admin username and password. Also, Keycloak internally runs on Port 8080 but I have mapped it to port 8890 to avoid conflicts with other services in my local machine.

Postgresql configuration is standard and follows the Keycloak docker guide.

  • One note is that I choose to use development version of Keycloak and have not enabled SSL to make the setup easy.
  • I should note that one playlist I found very useful for docker is from YouTube channel named code215 here: https://www.youtube.com/playlist?list=PLQZfys2xO5kgpa9-qpJly78d-t7_Fnjec. This series is very good to learn docker and keycloak and also for implementing SSL, using nginx and using infinispan.

All the code is available at: https://github.com/jawadrashid2011/aws-lambda-final

Setting up Keycloak

I am assuming that you have already created a realm, and created a default user as described in guide here https://www.keycloak.org/getting-started/getting-started-docker which is demo in my case for realm name and user for user.

Additionally i created 2 more users for testing purposes. Passwords needs to be set up for all 3 accounts.

Additionally you need to create a client under your master realm which I named demo-client, uncheck ‘Standard Flow’ and ‘Direct Access Grant’ options, check Client authentication (which will turn on Service account roles) and check Authorization. See screenshot below:

Creating client

We will use the name we set as client name and client secret can be found in credentials tab for the client we created.

Introduction to Lambda NodeJS

Instructions are given below for testing Lambda locally and deploying it on AWS Lambda:

What is AWS Lambda? Well if you want to know you might find this YouTube video by Simplilearn useful:

AWS Lambda Node.js documentation describes the steps on how to deploy Node.js Lambda functions with container images here: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-image.html

The reason I chose to deploy the Lambda function to docker service is because I wanted to be able to test locally the Lambda function before deploying to AWS Lambda. Using docker image helped me setup a container quickly and test Lambda functions locally with my docker Keycloak instance.

Using AWS base image for Node.js

In the guide https://docs.aws.amazon.com/lambda/latest/dg/nodejs-image.html I chose to select AWS base image for Node.js as you can start from here and just deploy your lambda function to this container and you can test it quickly.

I chose to follow the instructions described in section named “Using an AWS base image for Node.js”. I am going to note down the steps I used to create the service for lambda for Node.js using AWS node image. The code I have provided already has the instructions integrated into it so if you are using my code you don’t need to follow along.

  1. Create a folder and go into it
mkdir example
cd example

2. Initialize npm and select the defaults.

npm init

3. Create a file named index.js which will contain your lambda function code. Here is the sample code to test your lambda instance which outputs Hello From Lambda!.

exports.handler = async (event) => {
const response = {
statusCode: 200,
body: JSON.stringify('Hello from Lambda!'),
};
return response;
};

4. Create a dockerfile and put the following code. Basically we are using the aws lambda Node.js version 20 image and copying our code i.e. index.js to the root and running the lambda function in the container using CMD.

FROM public.ecr.aws/lambda/nodejs:20

# Copy function code
COPY index.js ${LAMBDA_TASK_ROOT}

# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "index.handler" ]

5. Build the image

docker build --platform linux/amd64 -t docker-image:test .

6. Test the docker image by running the docker container.

docker run --platform linux/amd64 -p 9000:8080 docker-image:test

7. With your docker container running with your lambda function deployed just test the code by using Powershell or Linux/macOS commands given in the guide above. I am giving two commands for powershell below:

For testing the lambda function without input:

Invoke-WebRequest -Uri "http://localhost:9000/2015-03-31/functions/function/invocations" -Method Post -Body '{}' -ContentType "application/json"

For testing the lambda function with payload input

Invoke-WebRequest -Uri "http://localhost:9000/2015-03-31/functions/function/invocations" -Method Post -Body '{"payload":"hello world!"}' -ContentType "application/json"

In the sample code case the payload input won’t give any different output as it is not using input in input variable but we can use the event in our lambda function to process the input to lambda function.

8. Stop the container by running commands where containerid is the id of the container for lambda in docker ps.

docker ps
docker kill containerid

9. Finally I am not going to give steps but you can follow the steps in “Deploying the image” to deploy the lambda function to AWS lambda in guide: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-image.html#nodejs-image-instructions.

  • It basically involves use aws ecr to authenticate docker cli to Amazon ECR registry, creating the docker repository in Amazon ECR, tagging the docker image, and pushing it.
  • Once that is done creating the lambda function giving it our image url, and testing the function using aws lambda invoke method described.

Creating Keycloak AWS function in Node.js to connect to Keycloak and perform our actions

Basically the above instructions helped to create a Node.js AWS lambda container to deploy and test the lambda function code. Now I will customize it to work with our docker setup and also I will share the code for connecting to Keycloak to perform our tasks.

Before we start I will be using keycloak admin client library (https://www.npmjs.com/package/@keycloak/keycloak-admin-client) which allows to write code in Node.JS to connect to Keycloak using REST protocol and perform a lot of actions. I have written a guide for this before in my previous tutorial here: https://medium.com/@jawadrashid/how-to-use-rest-api-for-keycloak-admin-through-node-js-app-cfac0372eb4a#a179 which showed how to connect to Keycloak using Node.JS app.

  • First let’s see review the service we added in our docker compose file in the start which is needed to test Node.js Lambda code locally.

Part of docker-compose.yaml dealing with node lambda service.

lambda-node:
build: ./lambda-node
ports:
- "9000:8080"
depends_on:
- keycloak_demo
networks:
- keycloak_demo_dev_network

Dockerfile for lambda node application.

FROM public.ecr.aws/lambda/nodejs:20

COPY getSessions.mjs ${LAMBDA_TASK_ROOT}
COPY terminateSessions.mjs ${LAMBDA_TASK_ROOT}

COPY ./node_modules ${LAMBDA_TASK_ROOT}/node_modules

CMD [ "getSessions.handler" ]
# CMD [ "terminateSessions.handler" ]

package.json

{
"name": "lambda-node",
"version": "1.0.0",
"description": "",
"lambda1": "getSessions.mjs",
"lambda2": "terminateSessions.mjs",
"main": "getSessions.mjs",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"@keycloak/keycloak-admin-client": "25.0.2"
}
}
  • Basically we in docker-compose I have created a service which uses dockerfile for building the aws lambda node.js docker image. We will discuss Dockerfile shortly.
  • As the node.js aws lambda code deploys locally on 8080 I have mapped it to port 9000 externally as I will call through Postman to test code.
  • Also as it connects to keycloak I have added dependency on keycloak_demo service.
  • The network is the same that is being used by Keycloak and Postgresql in docker-compose.
  • Now coming to the Dockerfile. I have modified the dockerfile created in the aws lambda steps I described in previous section.
  • I have created 2 files 1 for getting sessions and 1 for terminating sessions named getSessions.mjs and terminateSessions.mjs. The reason I used mjs extension was because when I used js file it was giving me errors for running code outside module and creating mjs resolved that.
  • Also, If you get any problems you should delete node_modules folder and use npm install command to install node_modules for your machine.
  • Package.json is standard except I have described dependency of keycloak-admin-client to connect Node.js to Keycloak and main is important which I will discuss when running the code. It should be getSessions.mjs for getting sessions and terminateSessions.mjs for terminating sessions.
  • Lambda code for getSessions.mjs
import KcAdminClient from "@keycloak/keycloak-admin-client";

const BASE_URL = process.env.BASE_URL || "http://keycloak_demo:8080";
const KEYCLOAK_GRANT_TYPE = process.env.KEYCLOAK_GRANT_TYPE || "client_credentials";
const KEYCLOAK_CLIENT_ID = process.env.KEYCLOAK_CLIENT_ID || "demo-client";
const KEYCLOAK_CLIENT_SECRET = process.env.KEYCLOAK_CLIENT_SECRET || "CLIENT_SECRET_GOES_HERE";
const KEYCLOAK_REALM = process.env.KEYCLOAK_REALM || "Demo";


export const handler = async (event) => {
const username = event.username || "";
const realmName = event.realm || KEYCLOAK_REALM;

/* Setting Environment and defaults */

let output = [];

const kcAdminClient = new KcAdminClient({
baseUrl: BASE_URL,
});

await kcAdminClient.auth({
grantType: KEYCLOAK_GRANT_TYPE,
clientId: KEYCLOAK_CLIENT_ID,
clientSecret: KEYCLOAK_CLIENT_SECRET,
});

// List first page of users
kcAdminClient.setConfig({
realmName: realmName,
});

const current_user = await kcAdminClient.users.find({
username: username,
exact: true,
});
// console.log("User", current_user);
if (current_user.length == 0) {
// console.log("No User Found");
output = "No User Found";
} else {
const sessions = await kcAdminClient.users.listSessions({
id: current_user[0].id,
});
// console.log("Sessions", sessions);
output = sessions;
}

const response = {
statusCode: 200,
body: output,
};
return response;
};
  • This code uses @keycloak/keycloak-admin-client library to connect to Keycloak.
  • One important note is that for the client you created which is defined in master realm needs to have service roles added: view-clients, manage-users, query-user, view-users for both master and the realm you want to get sessions from which in my case is named “Demo” as shown in following screenshot
  • The first part of the above script is setting up the environment variables/default values. One thing to note is that BASE_URL we need to connect to keycloak. Keycloak is not running here on localhost from within docker instead we need to use the ip address of the keycloak server. As we have defined the keycloak_demo service from within docker-compose.yaml we can use http://keycloak_demo as the url as keycloak_demo will automatically translate it ip address. Also, I am giving port 8080 and not the external port of keycloak_demo which in my case is 8890 because we are within docker and on same network and when we go to ip address keycloak_demo we are accessing local service so we will use 8080.
  • In handler we get an event object which is AWS lambda passes the payload to this variable. In the following code I am getting username send when calling the lambda function and optionally if realm is set then that is set also.
const username = event.username || "";
const realmName = event.realm || KEYCLOAK_REALM;
  • kcAdminClient.auth authenticates to the keycloak server.
  • Next the following code sets the realmName for further queries to the realm we are interested in interacting with which in my case is named Demo. When we will get sessions or users it will use the realmName which is either Demo or the one that is passed by lambda payload.
  • First we find the user from the realm using the following code. I found the code by first looking at the Keycloak admin REST api here: https://www.keycloak.org/docs-api/25.0.2/rest-api/index.html#_users. Then we want to find by username which we can use the method GET /admin/realms/{realm}/user. As he have already set the realm we only need to give username parameter. Also, I looked at https://github.com/keycloak/keycloak/tree/main/js/libs/keycloak-admin-client github and found the code below.
const current_user = await kcAdminClient.users.find({
username: username,
exact: true,
});
  • Then if it is found that we get all sessions for that user by using users.listSessions by giving user id which we got from previous step.
const sessions = await kcAdminClient.users.listSessions({
id: current_user[0].id,
});
  • Finally I return the sessions for that user in body.
  • Lambda code for terminateSession
import KcAdminClient from "@keycloak/keycloak-admin-client";

const BASE_URL = process.env.BASE_URL || "http://keycloak_demo:8080";
const KEYCLOAK_GRANT_TYPE = process.env.KEYCLOAK_GRANT_TYPE || "client_credentials";
const KEYCLOAK_CLIENT_ID = process.env.KEYCLOAK_CLIENT_ID || "demo-client";
const KEYCLOAK_CLIENT_SECRET = process.env.KEYCLOAK_CLIENT_SECRET || "CLIENT_SECRET_GOES_HERE";
const KEYCLOAK_REALM = process.env.KEYCLOAK_REALM || "Demo";



export const handler = async (event) => {
const username = event.username || "";
const realmName = event.realm || KEYCLOAK_REALM;

/* Setting Environment and defaults */

let output = "";

const kcAdminClient = new KcAdminClient({
baseUrl: BASE_URL,
});

await kcAdminClient.auth({
grantType: KEYCLOAK_GRANT_TYPE,
clientId: KEYCLOAK_CLIENT_ID,
clientSecret: KEYCLOAK_CLIENT_SECRET,
});

// List first page of users
kcAdminClient.setConfig({
realmName: realmName,
});

const current_user = await kcAdminClient.users.find({
username: username,
exact: true,
});
// console.log("User", current_user);
if (current_user.length == 0) {
output = "No User Found";
// console.log(output);
} else {
const sessions = await kcAdminClient.users.listSessions({
id: current_user[0].id,
});
output = sessions;
if (sessions.length == 0) {
output = "No Sessions Found";
// console.log(output);
} else {
await kcAdminClient.users.logout({
id: current_user[0].id,
});
output = "Logout succesful. Total sessions terminated: " + sessions.length;
// console.log(output);

}
}

const response = {
statusCode: 200,
body: output,
};
return response;
};
  • The code for terminateSessions is similar excpet we use the following code to logout the user by giving user id
await kcAdminClient.users.logout({
id: current_user[0].id,
});

How to test our lambda function

In our docker file we have defined which function to run. To test getSessions I will keep CMD [ “getSessions.handler” ] uncommented and comment out terminateSessions CMD command. Also in package.json main should be set to “getSessions.mjs” like below

"main": "getSessions.mjs",
  • Let’s first fire up our docker containers
  • Execute the following commands:
docker-compose down --remove-orphans
docker-compose up --build -d
  • This will build all 3 services: keycloak_demo, postgres_keycloak_demo and lambda-node.
  • You will have to wait till Keycloak has started and go to http://localhost:8890/admin to access the admin and give username/password to login which in my case is admin/admin
  • If this is first time you are running docker for this project then create 2 users and the client for my master as described in section Setting up Keycloak.
  • Also, open up a new tab and go to http://localhost:8890/realms/Demo/account where Demo is your realm name.
  • Login with the user you created which is in my case is named user
  • Now we need to test our lambda Node.js function. We can do this by either running Postman request or running command in Powershell. Let me discuss each option

For Powershell:

Run the following command. Our server is running on localhost on port 9000. This command is from the aws lambda documetnation for Node.js https://docs.aws.amazon.com/lambda/latest/dg/nodejs-image.html. Important thing is the payload/body ‘{“username”: “user”, “realm”: “Demo”}’. Here username is the username of user we want to get sessions for and realm is the realm name where user exists. Here realm is optional. If not given it will use KEYCLOAK_REALM.

Invoke-WebRequest -Uri "http://localhost:9000/2015-03-31/functions/function/invocations" -Method Post -Body '{"username": "user", "realm": "Demo"}' -ContentType "application/json"

You can find the macOS/linux command from the aws docs given above.

Response will be like this:

 {"statusCode":200,"body":[{"id":"b1f61547-8860-4de0-9e3c-3c5f88afd650","username":"user","userId":"536f08e5-0f63-458d-b0b2-
990735979a6b","ipAddress":"192.168.65.1","start":1722259486000,"lastAccess":1...

If there are multiple sessions for same user it will be in json comma separated. Here there is only 1 session for user currently.

For Postman

{
"username": "user",
"realm": "Demo"
}
  • Click send and you will get response like below:
{"statusCode":200,"body":
[{"id":"ec107b9d-734d-4690-9d95-8845aa417be8",
"username":"user",
"userId":"536f08e5-0f63-458d-b0b2-990735979a6b",
"ipAddress":"192.168.65.1",
"start":1722256373000,
"lastAccess":1722256375000,"rememberMe":false,"clients":{"64486b68-2e40-4adc-9f31-eae5c3491501":"account-console"},"transientUser":false}]}

How to test terminateSessions

  • In package.json inside lambda-node change “main” to “terminateSessions.mjs” like below:
"main": "terminateSessions.mjs",
  • In Dockerfile comment CMD for getSessions and remove comment from CMD for terminateSessions
# CMD [ "getSessions.handler" ]
CMD [ "terminateSessions.handler" ]
  • If docker is not running then use the same command to run the docker containers
docker-compose down --remove-orphans
docker-compose up --build -d
  • If docker is already running and you make changes to lambda-node service then just run the following code to recreate the lambda-node service with new changes without restarting Keycloak or db
docker-compose stop lambda-node
docker-compose up lambda-node --build -d
  • Next, test it similarly as you did for getSessions by executing command in Powershell or Postman without any change.
  • If there is any session for user that user will be logged out and you will get the following response which tells you how many sessions for the user is terminated:
{"statusCode":200,"body":"Logout succesful. Total sessions terminated: 1"}

Deploying the AWS Lambda To Amazon AWS

Follow the instructions given in guide by aws here: https://docs.aws.amazon.com/lambda/latest/dg/nodejs-image.html#nodejs-image-instructions. In Using an AWS base image for Node.js section go to part Deploying the image and follow the instructions. I don’t have a remote keycloak server running so I did not test deploying to AWS Lambda remotely but I tested the lambda functions locally through docker. Following the section below will deploy to aws lambda:

Final Thoughts

This part discusses how to create Node.JS AWS lambda function, test it locally in docker and deploy it. In next part here: https://medium.com/@jawadrashid/accessing-keycloak-api-using-java-and-aws-lambda-part-2-198e4e418707 we will discuss on how to do the same with Java lambda function. I will enhance the existing code with services for Java and Java app. The full code is available at: https://github.com/jawadrashid2011/aws-lambda-final

Resources

--

--

Jawad Rashid

A data scientist with background in full stack web development. My hobbies include learning new technology and working with mobile game development