Writing a Custom User Store Listener for WSO2 Identity Server đź«™

Nipuna Upeksha
Javarevisited
Published in
7 min read4 days ago

WSO2 Identity Server allows you to alter user store operations by registering an event listener for them. These listeners are executed at specific points in the user store process, and users can design listeners that implement the logic they want to run at these places. We can plug in any number of listeners, and they will be called one at a time. In this article, we will look at how to set up a custom user store listener for WSO2 Identity Server.

Although the latest release of WSO2 Identity Server is version 7.0 as of writing this article, I will be using WSO2 Identity Server 6.1 for our implementation. Furthermore, I will be using IntelliJ IDEA as the IDE, but you are free to use any IDE of your choice if you are following my steps to implement a custom user store listener.

Usually, the listeners are called whenever the user core method is called. The listeners can be registered before or after the actual method is called. For instance, think of a user operation named doOperation(). When we call that doOperation() method, we are actually calling two more methods, listeners.doPreOperation() and listeners.doPostOperation(). Therefore, we can use those two methods to listen to the core doOperation() method.

In this article, we will be implementing the following scenario. Whenever a user is getting logged in, the doAuthenticate() method will be invoked. We will be logging the username and logged-in time of that user with a custom user store listener.

Step 1: Create a new Maven project and add the necessary dependencies

Create a new Maven project using the below configurations for groupId and artifactId.

We will be creating the custom user store listener as an OSGi service. Therefore we do not need the Main.java file and hence you can delete that file. You can replace the contents of your pom.xml with the below-given pom.xml file. But make sure to update the groupId and artifactId properly.

<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.wso2.custom.userstore.listener</groupId>
<artifactId>org.wso2.custom.userstore.listener</artifactId>
<version>1.0-SNAPSHOT</version>
<name>Custom User Store Listener</name>
<packaging>bundle</packaging>

<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Maven Artifact Versions -->
<maven.compiler.plugin.version>2.0</maven.compiler.plugin.version>
<maven.bundle.plugin.version>3.2.0</maven.bundle.plugin.version>
<!-- Apache Versions -->
<commons.logging.version>1.2</commons.logging.version>
<!-- OSGi -->
<equinox.osgi.services.version>3.5.100.v20160504-1419</equinox.osgi.services.version>
<osgi.framework.imp.pkg.version.range>[1.7.0, 2.0.0)</osgi.framework.imp.pkg.version.range>
<osgi.service.component.imp.pkg.version.range>[1.2.0, 2.0.0)</osgi.service.component.imp.pkg.version.range>
<commons-logging.osgi.version.range>[1.2,2.0)</commons-logging.osgi.version.range>
<!-- WSO2 -->
<org.wso2.carbon.core.version>4.9.0</org.wso2.carbon.core.version>
</properties>


<dependencies>
<dependency>
<groupId>org.wso2.carbon</groupId>
<artifactId>org.wso2.carbon.core</artifactId>
<version>${org.wso2.carbon.core.version}</version>
</dependency>
<dependency>
<groupId>org.wso2.eclipse.osgi</groupId>
<artifactId>org.eclipse.osgi.services</artifactId>
<version>${equinox.osgi.services.version}</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>${commons.logging.version}</version>
</dependency>
</dependencies>

<repositories>
<repository>
<id>wso2-nexus</id>
<name>WSO2 internal Repository</name>
<url>https://maven.wso2.org/nexus/content/groups/wso2-public/</url>
<releases>
<enabled>true</enabled>
<updatePolicy>daily</updatePolicy>
<checksumPolicy>ignore</checksumPolicy>
</releases>
</repository>
<repository>
<id>wso2.releases</id>
<name>WSO2 internal Repository</name>
<url>https://maven.wso2.org/nexus/content/repositories/releases/</url>
<releases>
<enabled>true</enabled>
<updatePolicy>daily</updatePolicy>
<checksumPolicy>ignore</checksumPolicy>
</releases>
</repository>
<repository>
<id>wso2.snapshots</id>
<name>WSO2 Snapshot Repository</name>
<url>https://maven.wso2.org/nexus/content/repositories/snapshots/</url>
<snapshots>
<enabled>true</enabled>
<updatePolicy>daily</updatePolicy>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>


<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.plugin.version}</version>
<configuration>
<encoding>${project.build.sourceEncoding}</encoding>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<version>${maven.bundle.plugin.version}</version>
<extensions>true</extensions>
<configuration>
<instructions>
<Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
<Bundle-Name>${project.artifactId}</Bundle-Name>
<Private-Package>org.wso2.custom.userstore.listener.internal</Private-Package>
<Import-Package>
org.wso2.carbon.core.*,
org.osgi.framework; version="${osgi.framework.imp.pkg.version.range}",
org.osgi.service.component; version="${osgi.service.component.imp.pkg.version.range}",
org.apache.commons.logging; version="${commons-logging.osgi.version.range}",
</Import-Package>
<Export-Package>!org.wso2.custom.userstore.listener.internal,
org.wso2.custom.userstore.listener.*
</Export-Package>
<DynamicImport-Package>*</DynamicImport-Package>
</instructions>
</configuration>
</plugin>
</plugins>
</build>

</project>

If you want to create a custom user store listener for WSO2 Identity Server 7.0, make sure to update the dependency versions by cross-checking them properly with the pom.xml file in this repository (Make sure to check out the correct branch as well).

In the pom.xml file, you can see something like this.

<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<version>${maven.bundle.plugin.version}</version>
<extensions>true</extensions>
<configuration>
<instructions>
<Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
<Bundle-Name>${project.artifactId}</Bundle-Name>
<Private-Package>
org.wso2.custom.userstore.listener.internal
</Private-Package>
<Import-Package>
org.wso2.carbon.core.*,
org.osgi.framework; version="${osgi.framework.imp.pkg.version.range}",
org.osgi.service.component; version="${osgi.service.component.imp.pkg.version.range}",
org.apache.commons.logging; version="${commons-logging.osgi.version.range}",
</Import-Package>
<Export-Package>
!org.wso2.custom.userstore.listener.internal,
org.wso2.custom.userstore.listener.*
</Export-Package>
<DynamicImport-Package>*</DynamicImport-Package>
</instructions>
</configuration>
</plugin>

The reason for using the above configuration is explained in the previous article on Writing a Custom Event Handler for WSO2 Identity Server. Therefore I am not going to repeat myself here. You can check that article from here.

Furthermore, if you want to know more about OSGi microservices, you can check the below article as well.

Step 2: Writing Custom User Store Listener

To write a custom user store listener we need to extend org.wso2.carbon.user.core.common.AbstractUserOperationEventListener class. We can create a new class named, CustomUserStoreListener.java and extend the aforementioned class.

package org.wso2.custom.userstore.listener;

import org.wso2.carbon.user.core.common.AbstractUserOperationEventListener;

public class CustomUserStoreListener extends AbstractUserOperationEventListener {
}

Next, we can override the getExecutionOrderId() method to return a random value. This is important because when there is more than one listener in WSO2 IS, you need to consider their execution order.

package org.wso2.custom.userstore.listener;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.user.core.common.AbstractUserOperationEventListener;

public class CustomUserStoreListener extends AbstractUserOperationEventListener {

private static final Log log = LogFactory.getLog(CustomUserStoreListener.class);

@Override
public int getExecutionOrderId() {
return 9000;
}
}

Next, we need to implement our logic with the doPostAuthenticate() method. This method will be called after an actual user authentication is done. We can add the below code so that every time a user is authenticated, we can see a log of that user’s username and logged-in time.

package org.wso2.custom.userstore.listener;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.user.core.UserStoreException;
import org.wso2.carbon.user.core.UserStoreManager;
import org.wso2.carbon.user.core.common.AbstractUserOperationEventListener;

public class CustomUserStoreListener extends AbstractUserOperationEventListener {

private static final Log log = LogFactory.getLog(CustomUserStoreListener.class);

@Override
public int getExecutionOrderId() {
return 9000;
}

@Override
public boolean doPostAuthenticate(String userName, boolean authenticated, UserStoreManager userStoreManager)
throws UserStoreException {

// check whether user is authenticated
if(authenticated){

log.info("=== doPostAuthenticate ===");
log.info("User " + userName + " logged in at " + System.currentTimeMillis());
log.info("=== /doPostAuthenticate ===");

}

return true;
}
}

Step 3: Register the User Store Listener

Next, we need to register the event listener, so that it would behave as an OSGi component. To do that, we need to create a new package called internal and create a new class named CustomUserStoreListenerServiceComponent.java After creating the class, you can add the following code there. This code is important to register our user store listener as an OSGi service. Since I don't want to discuss why we need to create a service component and whatnot, I won’t be explaining that in here. But if you want to know why we need to create a service component for OSGi services, you can check my previous article on OSGi services.

package org.wso2.custom.userstore.listener.internal;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.wso2.carbon.user.core.listener.UserOperationEventListener;
import org.wso2.custom.userstore.listener.CustomUserStoreListener;

@Component(
name = "org.wso2.custom.userstore.listener",
immediate = true
)
public class CustomUserStoreListenerServiceComponent {
private static final Log log = LogFactory.getLog(CustomUserStoreListenerServiceComponent.class);

@Activate
protected void activate(ComponentContext context) {

CustomUserStoreListener listener = new CustomUserStoreListener();
context.getBundleContext().registerService(UserOperationEventListener.class.getName(),
listener, null);
log.debug("Custom user store listener activated successfully.");
}

@Deactivate
protected void deactivate(ComponentContext context) {
log.debug("Custom user store listener is deactivated");
}
}

Step 4: Build and Test

To build the custom component, execute the following command in the terminal.

mvn clean install -DskipTests

After that, copy the org.wso2.custom.userstore.listener-1.0-SNAPSHOT.jar from the target directory and paste it inside the <IS_HOME>/repository/components/dropins directory of a fresh IS 6.1 pack downloaded from here.

Then, add the following configurations in the <IS_HOME>/repository/conf/deployment.toml file, so that we can point out we are using a custom user store listener.

[[event_listener]]
id = "custom-userstore-listener"
type = "org.wso2.carbon.user.core.listener.UserOperationEventListener"
name = "org.wso2.custom.userstore.listener.CustomUserStoreListener"
order = 9000
enable = true

Next, go to the <IS_HOME>/bin directory and run the WSO2 Identity Server.

  • Linux/Mac Users → ./wso2server.sh -DosgiConsole
  • Windows → wso2server.bat -DosgiConsole

Use ss <component_name> command to check whether the custom user store listener is in ACTIVE state.

If it is not in ACTIVE state use b <component_id> and diag <component_id> command to resolve the issues.

Now, go to https://localhost:9443/carbon and log in to the WSO2 Management Console with the default username admin and password admin to check whether we can observe a log generated from our custom user store listener.

If everything works correctly, you will be able to see a log indicating the logged-in user and logged-in time in the terminal.

So this is it! This is how you can write a custom user store listener for the WSO2 Identity Server. If you want more details related to user store listeners, you can find them in the official documentation.

You can find the implemented custom user store listener from here.

--

--

Nipuna Upeksha
Javarevisited

Software Engineer | Visiting Lecturer | AWS SAA | MSc. in Big Data Analytics