Accessing Keycloak API using Java and AWS Lambda — Part 2

Jawad Rashid
7 min readJul 29, 2024

--

Introduction

This is a continuation for Part 1 where I created docker with Keycloak with Node.JS lambda function. I will extend the previous tutorial here: https://medium.com/@jawadrashid/accessing-keycloak-api-using-node-js-and-aws-lambda-part-1-5782437afaf2. You can find the Keycloak Java documentation here: https://www.keycloak.org/docs-api/25.0.2/javadocs/

Table of Contents

Docker Setup

The docker setup is similar to part 1 for Node.js with 2 services added for Java sessions applications. The updated docker-compose.yaml is below:

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

lambda-java-getsessions:
build: ./lambda-java/getsessions
ports:
- "9100:8080"
depends_on:
- keycloak_demo
networks:
- keycloak_demo_dev_network

lambda-java-terminatesessions:
build: ./lambda-java/terminatesessions
ports:
- "9200:8080"
depends_on:
- keycloak_demo
networks:
- keycloak_demo_dev_network


volumes:
pgdata_keycloak_data:
networks:
keycloak_demo_dev_network:
driver: bridge
  • Here I have added 2 services one for the getting services for java and one for terminate sessions.
  • There are two folders inside lambda-java folder. One is named getsessions which is for getting sessions java app and the other is terminatesssions app for terminating sessions.
  • The Dockerfile in each folder is same and given below. I will explain the code later on:
FROM public.ecr.aws/lambda/java:21

# Copy function code and runtime dependencies from Maven layout
COPY target/classes ${LAMBDA_TASK_ROOT}
COPY target/dependency/* ${LAMBDA_TASK_ROOT}/lib/

# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "com.keycloak.sessionsapp.App::handleRequest" ]

How to create Java app For AWS Lambda

The instructions on how to create Java app for AWS lambda is given here: https://docs.aws.amazon.com/lambda/latest/dg/java-image.html#java-image-instructions

Next we will create a project for Java. (You don’t need to follow these steps if you are using my code but if you want to create your own then you should follow these steps)

  1. Run linux or Powershell command to create a project. Replace groupid with your project group id, artifact id should be the name of final output and region to where you want to deploy:
mvn -B archetype:generate \
-DarchetypeGroupId=software.amazon.awssdk \
-DarchetypeArtifactId=archetype-lambda -Dservice=s3 -Dregion=US_WEST_2 \
-DgroupId=com.example.myapp \
-DartifactId=myapp
mvn -B archetype:generate `
"-DarchetypeGroupId=software.amazon.awssdk" `
"-DarchetypeArtifactId=archetype-lambda" "-Dservice=s3" "-Dregion=US_WEST_2" `
"-DgroupId=com.example.myapp" `
"-DartifactId=myapp"

2. Go to your main source file myapp/src/main/java/com/example/myapp and open App.java. It contains sample code which just outputs the payload.

3. Create Dockerfile

FROM public.ecr.aws/lambda/java:21

# Copy function code and runtime dependencies from Maven layout
COPY target/classes ${LAMBDA_TASK_ROOT}
COPY target/dependency/* ${LAMBDA_TASK_ROOT}/lib/

# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "com.example.myapp.App::handleRequest" ]

4. Compile

mvn compile dependency:copy-dependencies -DincludeScope=runtime

5. Build docker image

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

6. To test run command:

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

7. Test the code locally by running the following command with payload(You can call without payload if not needed):

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

8. You will get response with your payload output in json format as that is what the lambda function is currently doing.

9. You can deploy it to aws lambda service by following section Deploying the image on https://docs.aws.amazon.com/lambda/latest/dg/java-image.html#java-image-instructions

Our Java Lambda Function Code

  • Following the guide in last section I created seperate applications for gettingsesssions and terminatingsessions
  • Dockerfile in each folder is below:
FROM public.ecr.aws/lambda/java:21

# Copy function code and runtime dependencies from Maven layout
COPY target/classes ${LAMBDA_TASK_ROOT}
COPY target/dependency/* ${LAMBDA_TASK_ROOT}/lib/

# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "com.keycloak.sessionsapp.App::handleRequest" ]
  • Basically in the docker file we are using aws lambda java version 21. Then we are copying the classes and dependencies in correct place.
  • Finally we are calling our lambda function in container.

In each pom.xml other than the standard generated dependencies we need to add keycloak admin client library by adding following dependency(the version should match your keycloak version)

<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-client</artifactId>
<version>25.0.2</version>
</dependency>

Before explaining the App.java functionality let me show the docker-compose file that is deploying both of the java apps.

lambda-java-getsessions:
build: ./lambda-java/getsessions
ports:
- "9100:8080"
depends_on:
- keycloak_demo
networks:
- keycloak_demo_dev_network

lambda-java-terminatesessions:
build: ./lambda-java/terminatesessions
ports:
- "9200:8080"
depends_on:
- keycloak_demo
networks:
- keycloak_demo_dev_network
  • Here we are creating seperate services at port 9100 for getssisons and 9200 for terminatesessions.
  • Also, we are building the docker file I gave above which copies the generated classes and deploys it in container.

getsessions Java App

  • App.java for getsessions is below. We will discuss this in more detail:
package com.keycloak.sessionsapp;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;

import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.KeycloakBuilder;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

import software.amazon.awssdk.services.s3.S3AsyncClient;

/**
* Lambda function entry point. You can change to use other pojo type or implement
* a different RequestHandler.
*
* @see <a href=https://docs.aws.amazon.com/lambda/latest/dg/java-handler.html>Lambda Java Handler</a> for more information
*/
public class App implements RequestHandler<Object, Object> {
private final S3AsyncClient s3Client;

public App() {
// Initialize the SDK client outside of the handler method so that it can be reused for subsequent invocations.
// It is initialized when the class is loaded.
s3Client = DependencyFactory.s3Client();
// Consider invoking a simple api here to pre-warm up the application, eg: dynamodb#listTables
}

@Override
public Object handleRequest(final Object input, final Context context) {
String serverUrl = "http://keycloak_demo:8080";
String realmName = "Demo";
String mainAdminRealm = "master";

LinkedHashMap<String, String> inputHashMap = (LinkedHashMap<String, String>) input;
String user = inputHashMap.get("username");
if(inputHashMap.containsKey("realm")) {
realmName = inputHashMap.get("realm");
}

Keycloak keycloak = KeycloakBuilder.builder() //
.serverUrl(serverUrl) //
.realm(mainAdminRealm) //
.grantType(OAuth2Constants.CLIENT_CREDENTIALS) //
.clientId("demo-client") //
.clientSecret("CLIENT_SECRET_GOES_HERE") //
.build();

List<UserRepresentation> users = keycloak.realm(realmName)
.users()
.searchByUsername(user, true);




if(users.size() == 0) {
return "No User Found";
}

UserRepresentation foundUser = users.get(0);

List<UserSessionRepresentation> sessionsList = keycloak.realm(realmName).users().get(foundUser.getId()).getUserSessions();

if(sessionsList.size() == 0) {
return "No User Session Found";
}

List <String> userSessions = new ArrayList<String>();
for(UserSessionRepresentation session : sessionsList) {
String format = String.format("Id: %s, Username: %s, UserId: %s, ipAddress: %s, Start: %d, LastAccess: %d",
session.getId(), session.getUsername(), session.getUserId(), session.getIpAddress(), session.getStart(), session.getLastAccess());
System.out.println(format);
userSessions.add(format);
}
System.out.println(userSessions);



return sessionsList;
}

}

You need to change the following settings:

In both App.java in src/main/com/keycloak/sessionsapp under getsessions and terminatesessions folder under lambda-java there are some changes that need to be made. (You will need to change 2 files both App.java in sessions folders)

  • In handleRequest change serverUrl to your keycloak url.
  • Optionally you can change realmName to the realm you want to query but if you want you can give this realmName when invoking the AWS lambda function through terminal, aws command or Postman by setting realm value in body.
  • In lines 46–52 change clientId to your main master client id you created and client secret to your secret.

Other than the above changes no other change is needed but you need to look at the next section on how to compile the code to create classes and lib files.

Compiling Java code

Go to the root of the folder i.e. lambda-java/getsessions or lambda-java/terminatesessions in terminal and run the following command(You need to have maven installed on your computer)

mvn compile dependency:copy-dependencies -DincludeScope=runtime

After this is successful you will need to follow the instructions for deploying on docker to run the code.

Deploying Java Changes to Docker

  • If your docker containers for this project are not running then run the command below to build java code:
docker-compose down --remove-orphans
docker-compose up --build -d
  • If containers are running and you make changes in lambda-java folder src file then just run the following commands to recreate the lambda-java services:

For getsessions:

docker-compose stop lambda-java-getsessions
docker-compose up lambda-java-getsessions --build -d

For terminatesessions

docker-compose stop lambda-java-terminatesessions
docker-compose up lambda-java-terminatesessions --build -d

Testing the Java Lambda Function Locally Docker

For Powershell:

Run the following command. Our server is running on localhost on port 9100 for getsessions and 9200 for terminatesessions. 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.

For getsessions:

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

For terminatesessions

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

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,

For Postman follow the instructions in Postman section here: https://medium.com/@jawadrashid/accessing-keycloak-api-using-node-js-and-aws-lambda-part-1-5782437afaf2#8016

Deploying the AWS Lambda To Amazon AWS

Follow the instructions given in guide by aws here: https://docs.aws.amazon.com/lambda/latest/dg/java-image.html#java-image-instructions. In Using an AWS base image for Java 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 Remarks

You can find the whole code at: https://github.com/jawadrashid2011/aws-lambda-final. Also find he first Part 1 here: https://medium.com/@jawadrashid/accessing-keycloak-api-using-node-js-and-aws-lambda-part-1-5782437afaf2

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