Implementing Keycloak Event Listener SPI (Service Provider Interfaces)

Jawad Rashid
11 min readJul 10, 2024

--

Keycloak is an open source identity and access management solution. In this article I will explore and define steps of implementing Event Listener SPI in Java in Keycloak to catch events happening in Keycloak.

Table Of Contents

Introduction

You can implement a SPI like authentication, Action Token, Event Listener and much more. You can read more about SPI in Keycloak here https://www.keycloak.org/docs/latest/server_development/#_providers

Basically there is a long list of providers that Keycloak provides to provide custom code or extend already existing providers. Keycloak already provides a lot of providers for different parts of the software. You can provide your own provider to handle a part of the software you need access to.

If you have Keycloak already installed you can go to master realm and on the welcome page click on Provider Info page and you will see a list of SPI and providers for that SPI you can implement.

Keycloak logs and handles events like login, signup, sign out and much more through eventsListener SPI. I am going to implement that SPI by providing own code and including it in Keycloak in order to enable that SPI. eventsListener SPI capture admin and user events and I will handle successful login event from that.

There is a very good series for docker and Keycloak including implementing SPI on YouTube by a channel named code215 which includes implementing SPI in lesson 7. I would recommend following through all videos upto video 7 which will help you. The playlist is here: https://www.youtube.com/playlist?list=PLQZfys2xO5kgpa9-qpJly78d-t7_Fnjec . I have used another tutorial which I have linked below which discusses how to do everything for implementing SPI.

Implementing SPI in Java

Looking into Keycloak Server Developer guide (https://www.keycloak.org/docs/latest/server_development/#_providers) we can see that in order to implement an SPI we need 3 things:

  1. Provider class that implements the main login for the functionality.
  2. ProviderFactory class that instantiates an object of Provider class we implemented in step 1.
  3. A service configuration file which defines the exact factory we created in step 2 which Keycloak will use to get an object of our provider class.

An excerpt from the guide:

To implement an SPI you need to implement its ProviderFactory and Provider interfaces. You also need to create a service configuration file.

The guide above shows an example of ThemeSelector Provider, factory and the configuration to implement that SPI.

In our case for eventsListener we need to implement the following files:

  1. EventsListenerProvider
  2. EventListenerProviderFactory
  3. a file name org.keycloak.events.EventsListenerProviderFactory under src/main/resources/META-INF/services/

I will be reusing steps and code from this medium article by Adwait Thattey https://medium.com/@adwaitthattey/building-an-event-listener-spi-plugin-for-keycloak-5bf9de1b0965 which gives steps and detailed code for building Event Listener SPI for Keycloak. Have a look at that guide for more detailed understanding on how to implement event listener SPI.

  1. I will be using the eclipse IDE in order to create the Java maven project for implementing SPI. If you want to follow along install the latest Eclipse version installing Eclipse for Java Developers from the installation.
  2. Create an maven project in Eclipse (I selected simpe project — Skip archtectype selection) as I will be adding my own code in pom.xml
  3. Give a group Id (com.example) and artifact Id (simple-event-listener) for example and keep packaging as jar. We will customize all of these settings through pom.xml
  4. Now click finish. Click on pom.xml and edit it. Add the following code to pom.xml. I have changed some settings from the guide I gave above including the Keycloak version and some settings. I have set the version of keycloak-parent to 25.0.1 which is the latest keycloak version at the time of writing this article.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-parent</artifactId>
<version>25.0.1</version>
</parent>

<properties>
<java.version>1.8</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
</properties>

<name>REST Provider 1</name>
<description/>
<modelVersion>4.0.0</modelVersion>

<artifactId>sample_event_listener</artifactId>
<packaging>jar</packaging>

<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi-private</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-saml-core-public</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.ws.rs</groupId>
<artifactId>jboss-jaxrs-api_2.1_spec</artifactId>
</dependency>
</dependencies>

<build>
<finalName>sample-event-listener</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.wildfly.plugins</groupId>
<artifactId>wildfly-maven-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>

You can customize any settings you want. I added additional config for maven-jar-plugin.version as I was having a problem due to bug in Eclipse. Next right click on your project name in package explorer → maven → update project so all dependencies are downloaded.

5. Create a package under src/main/java. I created the package named com.keycloaktesting.sampleeventlistenerprovider.proivder. You can set your package to whatever you want that is suitable.

6. Add SampleEventListenerProvider.java file under src/main/java/com.keycloaktesting.sampleeventlistenerprovider.proivder package. The contents of the file is as following. I have used the same code with some modifications from guide by Adwait Thattey.

package com.keycloaktesting.sampleeventlistenerprovider.provider;

import java.util.Map.Entry;

import org.keycloak.events.Event;
import org.keycloak.events.EventListenerProvider;
import org.keycloak.events.admin.AdminEvent;

public class SampleEventListenerProvider implements EventListenerProvider {

public SampleEventListenerProvider() {

}

@Override
public void onEvent(Event event) {
if(event.getType().toString() == "LOGIN") {
System.out.println("Event Occurred:" + toString(event));
}
}

@Override
public void onEvent(AdminEvent adminEvent, boolean b) {

System.out.println("Admin Event Occurred:" + toString(adminEvent));
}

@Override
public void close() {

}

private String toString(Event event) {

StringBuilder sb = new StringBuilder();

sb.append("Event FOUND");

sb.append(", type=");

sb.append(event.getType());

sb.append(", realmId=");

sb.append(event.getRealmId());

sb.append(", clientId=");

sb.append(event.getClientId());

sb.append(", userId=");

sb.append(event.getUserId());

sb.append(", ipAddress=");

sb.append(event.getIpAddress());


// if (event.getError() != null) {
//
// sb.append(", error=");
//
// sb.append(event.getError());
//
// }


if (event.getDetails() != null) {

for (Entry<String, String> e : event.getDetails().entrySet()) {

sb.append(", ");

sb.append(e.getKey());

if (e.getValue() == null || e.getValue().indexOf(' ') == -1) {

sb.append("=");

sb.append(e.getValue());

} else {

sb.append("='");

sb.append(e.getValue());

sb.append("'");

}

}

}


return sb.toString();

}


private String toString(AdminEvent adminEvent) {

StringBuilder sb = new StringBuilder();


sb.append("operationType=");

sb.append(adminEvent.getOperationType());

sb.append(", realmId=");

sb.append(adminEvent.getAuthDetails().getRealmId());

sb.append(", clientId=");

sb.append(adminEvent.getAuthDetails().getClientId());

sb.append(", userId=");

sb.append(adminEvent.getAuthDetails().getUserId());

sb.append(", ipAddress=");

sb.append(adminEvent.getAuthDetails().getIpAddress());

sb.append(", resourcePath=");

sb.append(adminEvent.getResourcePath());


if (adminEvent.getError() != null) {

sb.append(", error=");

sb.append(adminEvent.getError());

}


return sb.toString();

}

}

Important things to note is that this class implements EventListenerProvider interface. This interface has two methods onEvent and onAdminEvent. OnAdminEvent is called every time an event in admin side happens and onEvent is called when some event happens on user side.

There are functions toString for admin and user to help display more information about the event. In onEvent I have added a if condition that is type of event is LOGIN which means that a user successfully logged in that display that event.

7. Create another file SampleEventListeneProviderFactory.java under the same folder as Provider.

package com.keycloaktesting.sampleeventlistenerprovider.provider;

import org.keycloak.Config.Scope;
import org.keycloak.events.EventListenerProvider;
import org.keycloak.events.EventListenerProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;

public class SampleEventListenerProviderFactory implements EventListenerProviderFactory {

@Override
public EventListenerProvider create(KeycloakSession session) {
return new SampleEventListenerProvider();
}

@Override
public void init(Scope config) {

}

@Override
public void postInit(KeycloakSessionFactory factory) {

}

@Override
public void close() {

}

@Override
public String getId() {
return "sample_event_listener";
}

}

This factory just instiantes our Listener Provider object in create method. As given in Server Developer Guide for Implementing SPI you need to defined a getId method which should return a unique id.

It is recommended that your provider factory implementation returns unique id by method getId(). However there can be some exceptions to this rule as mentioned below in the Overriding providers section.

7. Next we need to create src/main/resources folder if it does not exist. Under src/main/resources create folders META-INF and under that services. Create a file named org.keycloak.events.EventListenerProviderFactory

8. In the configuration file in step 7 add the following code. This should be the path of your provider factory which will in turn call your provider.

com.keycloaktesting.sampleeventlistenerprovider.provider.SampleEventListenerProviderFactory

9. You need to use maven to build and package your app so that you can get generated jars. First build the project if the project is not built. Next for packaging for me as I am using Eclipse I right click on pom.xml and click maven build. In goals add package. See the screenshot below. Press run. You should see a output in console in Eclipse that it has created jar files and build is successful.

Maven build and package.

10. Next go to your project in windows explorer, go to target folder and you should see 2 jar files at root levels named sample-event-listener.jar and sample-event-listener-sources.jar. You only need the first one.

You will need this jar while setting up keycloak. With this the provider part is done.

Dockerized Keycloak Setup

I am using Keycloak 25.0.1 at time of writing of this article. Instructions may vary if you are using a newer version of Keycloak. See documentation for changes. Next we need to setup keycloak in docker so we can deploy our provider in keycloak provider folder and activate it. The original guide mentions that you need to create keycloak by following guide below, create a realm, create a user under realm with password and deploy the jar.

The guide mentions that you need to do this:

The deployment process is pretty straightforward. We need to copy the sample-event-listener.jar to $KEYCLOAK_DIR/standalone/deployments/ where $KEYCLOAK_DIR is the main KeyCloak directory (after unzipping)

KeyCloak supports hot-reloading. So as soon we copy the jar file, keycloak should reload and deploy the plugin. But just to be sure, let’s restart the Keycloak server.

For me as I want to deploy keycloak in docker I had to go on a different route. In my previous article of using REST API in docker with Keycloak https://medium.com/@jawadrashid/how-to-use-rest-api-for-keycloak-admin-through-node-js-app-cfac0372eb4a#a179 I used the guide by Keycloak here https://www.keycloak.org/getting-started/getting-started-docker to setup Keycloak in docker.

The above guide does deploy Keycloak in docker but I needed to copy provider files jar to the providers folder in Keycloak so the above guide was not sufficient as it does not create a docker file and I wanted to use docker compose.

I used Keycloak guide here https://www.keycloak.org/server/containers for a starting point but still there were some missing pieces. So I decided to use the article https://saurav-samantray.medium.com/dockerize-keycloak-21-with-a-custom-theme-b6f2acad03d5 along with the above guide to dockerize the environment.

  1. Basically start off a new folder and create Dockerfile as given below:
FROM quay.io/keycloak/keycloak:latest as builder

ENV KC_DB=postgres

WORKDIR /opt/keycloak

# for demonstration purposes only, please make sure to use proper certificates in production instead
RUN keytool -genkeypair -storepass password -storetype PKCS12 -keyalg RSA -keysize 2048 -dname "CN=server" -alias server -ext "SAN:c=DNS:localhost,IP:127.0.0.1" -keystore conf/server.keystore

RUN /opt/keycloak/bin/kc.sh build

FROM quay.io/keycloak/keycloak:latest
COPY --from=builder /opt/keycloak/ /opt/keycloak/

COPY --chown=keycloak:keycloak --chmod=644 ./providers/ /opt/keycloak/providers/

ENTRYPOINT ["/opt/keycloak/bin/kc.sh"]

Here in this docker file we are using keycloak latest docker file as starting point and also we are copying the jar files in our providers folder to /opt/keycloak/providers/ directory under docker. The reason is that the guide mentions the following:

To install custom providers, you just need to define a step to include the JAR file(s) into the /opt/keycloak/providers directory

2. Create a providers directory in same folder that your dockerfile exists. Add the jar we created named sample-event-listener.jar to the providers folder so it can be copied into docker container.

3. Create an empty keycloak directory as well.

4. Create docker-compose.yaml at same level as Dockerfile. The contents are as below:

version: '3'

volumes:
keycloak-db-data:
driver: local

services:
postgres:
image: postgres:13.7
container_name: postgres
volumes:
- keycloak-db-data:/var/lib/postgresql/data
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: keycloak_admin
POSTGRES_PASSWORD: keycloak_password
keycloak:
build: .
container_name: custom-auth-service
environment:
#Admin Credentials
KC_HOSTNAME_STRICT: 'false'
KC_HOSTNAME_STRICT_HTTPS: 'false'
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: admin
KC_DB_URL_HOST: postgres
KC_DB_USERNAME: keycloak_admin
KC_DB_PASSWORD: keycloak_password
KC_HEALTH_ENABLED: 'true'
KC_METRICS_ENABLED: 'true'
KC_HOSTNAME: localhost
PROXY_ADDRESS_FORWARDING: "true"
command:
- "start-dev"
ports:
- 8443:8443
- 8080:8080
depends_on:
- postgres

We will deploy a main container with keycloak named custom-auth-service and another container for postgresql as we are using postgresql for database. You can customize postgresql settings in yaml file above. One thing to note is that we are executing command start-dev as we want to start the server in development mode.

5. Use the following command to build and run keycloak with database in docker.

docker-compose up --build

6. When Keycloak runs in docker. You might see message as below that it has detected eventsListener.

sample_event_listener (com.keycloaktesting.sampleeventlistenerprovider.provider.SampleEventListenerProviderFactory) is implementing the internal SPI eventsListener. This SPI is internal and may change without notice

7. You need to create a realm, create a user and set password by following the guide: https://www.keycloak.org/getting-started/getting-started-docker

8. Once you have done all the steps in guide in step 7 then go to http://localhost:8080/admin/ login through your keycloak admin. Select your custom realm from dropdown on top left. For me my realm was myrealm.

9. Go to events page under manage section in sidebar

10. Click on Event configs link in the paragraph in events page. You can go directly to this url http://localhost:8080/admin/master/console/#/myrealm/realm-settings/events replacing myrealm with your realm name in url.

11. In events listener start typing sample and in event listeners and select sample_event_listener. This is the event listener we created by implementing SPI.

Add sample_event_listener in event listeners.

12. Now open up your docker container custom-auth-service logs section.

Logs under custom-auth-service

13. Clear your docker logs by clicking on delete button in logs section on right so you will be able to see the message you created on login clearly.

14. Open up http://localhost:8080/realms/myrealm/account where myrealm is your realm name and login with the user you created while creating keycloak guide in step 7.

15. Go back to logs under docker and you should see message as below. This message was generated by our SPI code. As I have set up a condition that only LOGIN event should be displayed you will see only LOGIN event. This message means that our code is working and Keycloak is using our provider.

Event Occurred:Event FOUND, type=LOGIN, 
realmId=560a9b9d-2917-...,
clientId=account-console,
userId=13af8b9e-fa44-...,
ipAddress=192.168....,
auth_method=openid-connect,
auth_type=code,
redirect_uri=http://localhost:8080/realms/myrealm/account,
consent=no_consent_required,
code_id=5cb473a8-35c8-...,
username=myuser

All the code is available at https://github.com/jawadrashid2011/keycloak-spi-eventlistener. Look in resources section for further guides from where I have taken code from. Want to read something else read my last article here: How to use REST API for Keycloak Admin Through Node JS App

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