Keycloak offers a range of Service Provider Interfaces (SPIs) that enable specific functionalities within the server. By following this guide, you’ll understand how to code and install a custom Provider that you can add to your Keycloak installation.
Keycloak Providers in a nutshell
In Keycloak, a provider is an extension that adds specific functionality to the core system. These providers can handle various tasks, such as authentication, user federation, event handling, and more. They offer a way to customize Keycloak’s behavior and integrate it with different systems.Here’s a breakdown of Keycloak Providers:
- Event Listeners: These providers listen for events happening within Keycloak, such as user login, logout, or creation. They can then perform actions based on these events, like sending notifications or logging data.
- Authentication Providers: These providers handle user authentication against external identity sources like LDAP servers, social identity providers (e.g., Google, Facebook), or custom authentication mechanisms.
- User Federation Providers: These providers allow you to synchronize user data between Keycloak and other user directories or databases.
- SPI (Service Provider Interface) Implementations: Keycloak offers various Service Provider Interfaces (SPIs) for extending its functionalities in specific areas. These SPIs have corresponding provider implementations that plug into the core system.
Coding a simple Event Listener Provider
The simplest form of Provider is the Event Listener. In order to create your Event Listener Provider, you need to implement the following interfaces:
- EventListenerProvider Interface: This interface contains the callback methods when the event is triggered.
- EventListenerFactory: This factory allows the creation of instances of the
EventListenerProvider
.
An EventListener Provider example
This interface is used to handle events occurring within Keycloak. It has two methods:
onEvent(Event event)
: This method is invoked whenever an event occurs in Keycloak. You can perform custom actions based on the event.onEvent(AdminEvent event, boolean includeRepresentation)
: Similar toonEvent(Event event)
, but specifically for administrative events.
Here is a sample implementation from Keycloak quickstarts:
public class SysoutEventListenerProvider implements EventListenerProvider { private Set<EventType> excludedEvents; private Set<OperationType> excludedAdminOperations; public SysoutEventListenerProvider(Set<EventType> excludedEvents, Set<OperationType> excludedAdminOpearations) { this.excludedEvents = excludedEvents; this.excludedAdminOperations = excludedAdminOpearations; } @Override public void onEvent(Event event) { // Ignore excluded events if (excludedEvents != null && excludedEvents.contains(event.getType())) { return; } else { System.out.println("EVENT: " + toString(event)); } } @Override public void onEvent(AdminEvent event, boolean includeRepresentation) { // Ignore excluded operations if (excludedAdminOperations != null && excludedAdminOperations.contains(event.getOperationType())) { return; } else { System.out.println("EVENT: " + toString(event)); } } private String toString(Event event) { StringBuilder sb = new StringBuilder(); 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 (Map.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(); } @Override public void close() { } }
In this example, the SysoutEventListenerProvider
logs events to the console. It filters out events based on the exclusion lists provided during initialization.
EventListenerProviderFactory Interface
This interface is a factory to create instances of the EventListenerProvider
. It has the following methods:
create(KeycloakSession session)
: Creates an instance of theEventListenerProvider
. You’ll typically instantiate your custom provider here.init(Config.Scope config)
: Initializes the factory with configuration parameters provided by Keycloak. This method is used to set up any required configurations.postInit(KeycloakSessionFactory factory)
: Called after all factories have been initialized. Useful for performing any post-initialization tasks.close()
: Cleans up any resources used by the factory.getId()
: Returns a unique identifier for the factory.
Here is a sample implementation:
public class SysoutEventListenerProviderFactory implements EventListenerProviderFactory { private Set<EventType> excludedEvents; private Set<OperationType> excludedAdminOperations; @Override public EventListenerProvider create(KeycloakSession session) { return new SysoutEventListenerProvider(excludedEvents, excludedAdminOperations); } @Override public void init(Config.Scope config) { String[] excludes = config.getArray("exclude-events"); if (excludes != null) { excludedEvents = new HashSet<>(); for (String e : excludes) { excludedEvents.add(EventType.valueOf(e)); } } String[] excludesOperations = config.getArray("excludesOperations"); if (excludesOperations != null) { excludedAdminOperations = new HashSet<>(); for (String e : excludesOperations) { excludedAdminOperations.add(OperationType.valueOf(e)); } } } @Override public void postInit(KeycloakSessionFactory factory) { } @Override public void close() { } @Override public String getId() { return "sysout"; } }
Installing the Provider on Keycloak
In order to install a provider, you need to copy its implementation (in the JAR file) into the providers folder of your Keycloak distribution. For example:
cp target/event-listener-sysout.jar /opt/keycloak-24.0.1/providers/
Then, start Keycloak. You can optionally include a list of events which you don’t want to capture with your listener. For example, to exclude the REFRESH_TOKEN and CODE_TO_TOKEN events, you can add them to the spi-events-listener-sysout-exclude-events
option:
./kc.sh start-dev --spi-events-listener-sysout-exclude-events=CODE_TO_TOKEN,REFRESH_TOKEN
Enabling the Event
Lastly, you need to enable the Event Listener. In order to do so, from the Keycloak Console select Realm Settings and the Events Tab. Add the sysout
Event Listener to your List of Events:
Click Save.
Now, when you produce events such as login/logout or other admin events you will see that captured by your Event Listener which prints it on the console:
ADMIN EVENT: operationType=UPDATE, realmId=f305d6c0-df99-416a-9f7d-cdab4b90a41f, clientId=15cbfc55-9b70-439b-8a46-22805a3bf295, userId=d68698b6-e924-4fdb-b8ee-ca3d97113ff3, ipAddress=127.0.0.1, resourcePath=events/config
Conclusion
Overall, Keycloak providers are powerful tools for extending the platform’s capabilities and tailoring it to your specific use case. This article discussed how to build and deploy a sample Logging Listener in your Keycloak distribution.