Photo by Frank Septillion on Unsplash
Writing a Custom User Store Listener for WSO2 Identity Server 🫙
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 this 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.