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.

Project Structure

  • Server Provider Interfaces guide: link
  • Tabunganku BE Github repository: link