One of the most common use cases before migrating to Keycloak is syncing data between Keycloak and existing user data. Here, we are going to implement a custom event listener to address the issue.
Implementation#
I recommend to implement the event listener in a separate module or project.
We need to create new classes that implement the interfaces provided by Keycloak, EventListenerProvider
and EventListenerProviderFactory
, in order to have a custom event listener. EventListenerProviderFactory
includes functions to help identify the event listener ID and instantiate an EventListenerProvider
class, whereas EventListenerProvider
includes functions for handling events that pass through Keycloak. We use keycloak-dependencies-server-all
library as a dependency.
Let’s see the implementation of the EventListenerProviderFactory
class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| private static final String PROVIDER_ID = "tabungan-db-sync";
@Override
public EventListenerProvider create(KeycloakSession keycloakSession) { //... (1)
return new ExternalDbSyncProvider();
}
@Override
public void init(Config.Scope scope) {}
@Override
public void postInit(KeycloakSessionFactory keycloakSessionFactory) {}
@Override
public void close() {}
@Override
public String getId() { //... (2)
return PROVIDER_ID;
}
|
There are two functions used here: create
and getId
, which functions as their name implies. create
function creates a new ExternalDbSyncProvider
object, where data synchronization occurs.
ExternalDbSyncProvider
class is a custom event listener class that implements EventListenerProvider
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| private final String REALM_ID = System.getenv("TABUNGAN_REALM_ID");
private final String DB_URL = MessageFormat.format("{0}/{1}",
System.getenv("JDBC_DB_URL"),
System.getenv("TABUNGAN_DB_NAME"));
private final String DB_USER = System.getenv("JDBC_DB_USERNAME");
private final String DB_PASSWORD = System.getenv("JDBC_DB_PASSWORD");
private final String DB_SQL = "INSERT INTO users (user_id, first_name, last_name, email, username) VALUES (?,?,?,?,?)";
public ExternalDbSyncProvider() {};
@Override
public void onEvent(Event event) {
if (event.getRealmId().equals(REALM_ID) && event.getType() == EventType.REGISTER) {
try (Connection conn = DbConnect(); var pstmt = conn.prepareStatement(DB_SQL)) {
pstmt.setObject(1, UUID.fromString(event.getUserId()));
pstmt.setString(2, event.getDetails().get("first_name"));
pstmt.setString(3, event.getDetails().get("last_name"));
pstmt.setString(4, event.getDetails().get("email"));
pstmt.setString(5, event.getDetails().get("username"));
int affectedRows = pstmt.executeUpdate();
} catch (SQLException e) {
System.out.println(e.getMessage());
throw new RuntimeException(e);
}
}
}
@Override
public void onEvent(AdminEvent adminEvent, boolean b) {}
@Override
public void close() {}
private Connection DbConnect() {
Connection conn = null;
try {
conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
} catch (SQLException e) {
System.out.println(e.getMessage());
}
return conn;
}
|
The snippet above is the implementation of ExternalDbSyncProvider
class. onEvent
method is where the execution takes place. It checks whether a user registration event originates from a specific realm ID. If this condition is true, it will insert a new user data into the existing database. Meanwhile, DbConnect
is used to establish a connection between Keycloak and the user’s database.
JAR Packaging#
Before packaging the classes into a JAR, we need to add a new file at src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory
with the value com.tabunganku.sync.ExternalDbSyncProviderFactory
, which is the full class name of the class that implements the EventListenerProviderFactory
interface. The JAR file can be created by running the mvn clean install
command.
Useful Links#
- Server Provider Interfaces guide: link
- Tabunganku BE Github repository: link