Writing a Custom User Store Manager for WSO2 Identity Server 📦

Photo by Nate Grant on Unsplash

Writing a Custom User Store Manager for WSO2 Identity Server 📦

·

8 min read

User stores are places where you store user information. However, sometimes, the default user store schema might not work with your use cases. For instance, suppose you have a company that already has a user database and only needs to authenticate the user through the WSO2 Identity Server. In that case, you don’t have to convert your whole database to the WSO2 Identity Server standard schema.

Therefore, WSO2 Identity Server allows you to create custom user store managers. In this article, we will be creating a custom user store manager that will hash the password and save it in the user store using the third-party Jasypt library.

As of writing this article, WSO2 Identity Server 7.0 is the latest version. However, I will be using WSO2 Identity Server 6.1 throughout this article. Also, I will be using IntelliJ IDEA as my IDE, but feel free to use any IDE of your choice, if you want to implement this custom user store manager with me.

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

Open IntelliJ IDEA and create a new Maven project with the following configurations.

Since we will be creating the custom user store manager as an OSGi service we do not need the Main.java file. Therefore, you can remove that file from the project. Next, we need to update the pom.xml file with the necessary dependencies. To do that, you can replace the contents of your pom.xml file with the below pom.xml configurations. However, make sure to update the artifactId and groupId to match the initial artifactId and groupId of your project.

<?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.manager</groupId>
    <artifactId>org.wso2.custom.userstore.manager</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>Custom Userstore Manager</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 -->
        <carbon.kernel.version>4.9.0</carbon.kernel.version>
        <carbon.kernel.package.import.version.range>[4.6.0, 5.0.0)</carbon.kernel.package.import.version.range>
        <carbon.user.api.imp.pkg.version.range>[1.0.1, 2.0.0)</carbon.user.api.imp.pkg.version.range>
        <axiom.imp.pkg.version>[1.2.11, 1.3.0)</axiom.imp.pkg.version>
        <commons-lang.wso2.version>2.6.0.wso2v1</commons-lang.wso2.version>
        <commons-lang.wso2.osgi.version.range>[2.6.0,3.0.0)</commons-lang.wso2.osgi.version.range>
        <!-- Others -->
        <org.jasypt.version>1.9.2</org.jasypt.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.wso2.carbon</groupId>
            <artifactId>org.wso2.carbon.user.api</artifactId>
            <version>${carbon.kernel.version}</version>
        </dependency>
        <dependency>
            <groupId>org.wso2.carbon</groupId>
            <artifactId>org.wso2.carbon.user.core</artifactId>
            <version>${carbon.kernel.version}</version>
        </dependency>
        <dependency>
            <groupId>org.jasypt</groupId>
            <artifactId>jasypt</artifactId>
            <version>${org.jasypt.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.internal.*,
                            org.jasypt.*
                        </Private-Package>
                        <Export-Package>
                            !org.wso2.custom.userstore.internal,
                            org.wso2.custom.userstore.*"
                        </Export-Package>
                        <Import-Package>
                            org.osgi.framework.*;version="${osgi.framework.imp.pkg.version.range}",
                            org.osgi.service.component.*;version="${osgi.service.component.imp.pkg.version.range}",

                            org.apache.axiom.om.util.*; version="${axiom.imp.pkg.version}",
                            org.apache.commons.lang; version="${commons-lang.wso2.osgi.version.range}",
                            org.apache.commons.logging.*; version="${commons-logging.osgi.version.range}",

                            org.wso2.carbon.user.core.*; version="${carbon.kernel.package.import.version.range}",
                            org.wso2.carbon.user.api.*; version="${carbon.user.api.imp.pkg.version.range}",
                            org.wso2.carbon.utils.*; version="${carbon.kernel.package.import.version.range}",
                            *;resolution:=optional
                        </Import-Package>
                        <DynamicImport-Package>*</DynamicImport-Package>
                    </instructions>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

As you can see we are using the third-party Jasypt library with the following dependency block.

<dependency>
  <groupId>org.jasypt</groupId>
  <artifactId>jasypt</artifactId>
  <version>${org.jasypt.version}</version>
</dependency>

In addition, you will note the following code snippet in the pom.xml file as well.

<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.internal.*, 
        org.jasypt.* 
      </Private-Package>
      <Export-Package> 
        !org.wso2.custom.userstore.internal, 
        org.wso2.custom.userstore.*" 
      </Export-Package>
      <Import-Package> 
        org.osgi.framework.*;version="${osgi.framework.imp.pkg.version.range}", 
        org.osgi.service.component.*;version="${osgi.service.component.imp.pkg.version.range}", 
        org.apache.axiom.om.util.*; version="${axiom.imp.pkg.version}", 
        org.apache.commons.lang; version="${commons-lang.wso2.osgi.version.range}", 
        org.apache.commons.logging.*; version="${commons-logging.osgi.version.range}", 
        org.wso2.carbon.user.core.*; version="${carbon.kernel.package.import.version.range}", 
        org.wso2.carbon.user.api.*; version="${carbon.user.api.imp.pkg.version.range}", 
        org.wso2.carbon.utils.*; version="${carbon.kernel.package.import.version.range}", *;resolution:=optional 
      </Import-Package>
      <DynamicImport-Package>*</DynamicImport-Package>
    </instructions>
  </configuration>
</plugin>

Since I have already explained why we need this configuration in a previous article, I am not going to repeat myself here. But if you want to check that article you can check it from here.

Furthermore, if you want to know how to write OSGi services, you can check my article on OSGi services from here.

Step 2: Write Custom User Store Manager

To create a custom user store manager we need to extend the user store manager class that is compatible with the user store. For instance, if we are going to use the JDBC user store manager we need to extend the UniqueIDJDBCUserStoreManager class. Since we are going to use a JDBC user store manager in this article, we can create a new class named CustomUserStoreManager.java and extend it with UniqueIDJDBCUserStoreManager class.

package org.wso2.custom.userstore.manager;

import org.wso2.carbon.user.core.jdbc.UniqueIDJDBCUserStoreManager;

public class CustomUserStoreManager extends UniqueIDJDBCUserStoreManager {
}

Next, override the doAuthenticateWithUserName method to write custom authentication logic and prepatePassword method to hash the passwords using Jasypt.

package org.wso2.custom.userstore.manager;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jasypt.util.password.StrongPasswordEncryptor;
import org.wso2.carbon.user.api.RealmConfiguration;
import org.wso2.carbon.user.core.UserCoreConstants;
import org.wso2.carbon.user.core.UserRealm;
import org.wso2.carbon.user.core.UserStoreException;
import org.wso2.carbon.user.core.claim.ClaimManager;
import org.wso2.carbon.user.core.common.AuthenticationResult;
import org.wso2.carbon.user.core.common.FailureReason;
import org.wso2.carbon.user.core.common.User;
import org.wso2.carbon.user.core.jdbc.JDBCRealmConstants;
import org.wso2.carbon.user.core.jdbc.UniqueIDJDBCUserStoreManager;
import org.wso2.carbon.user.core.profile.ProfileConfigurationManager;
import org.wso2.carbon.utils.Secret;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Map;

/**
 * This class implements the Custom User Store Manager.
 */
public class CustomUserStoreManager extends UniqueIDJDBCUserStoreManager {

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

    private static final StrongPasswordEncryptor passwordEncryptor = new StrongPasswordEncryptor();


    public CustomUserStoreManager() {

    }

    public CustomUserStoreManager(RealmConfiguration realmConfig, Map<String, Object> properties, ClaimManager
            claimManager, ProfileConfigurationManager profileManager, UserRealm realm, Integer tenantId)
            throws UserStoreException {

        super(realmConfig, properties, claimManager, profileManager, realm, tenantId);
        log.info("CustomUserStoreManager initialized...");
    }


    @Override
    public AuthenticationResult doAuthenticateWithUserName(String userName, Object credential)
            throws UserStoreException {

        boolean isAuthenticated = false;
        String userID = null;
        User user;
        // In order to avoid unnecessary db queries.
        if (!isValidUserName(userName)) {
            String reason = "Username validation failed.";
            if (log.isDebugEnabled()) {
                log.debug(reason);
            }
            return getAuthenticationResult(reason);
        }

        if (!isValidCredentials(credential)) {
            String reason = "Password validation failed.";
            if (log.isDebugEnabled()) {
                log.debug(reason);
            }
            return getAuthenticationResult(reason);
        }

        try {
            String candidatePassword = String.copyValueOf(((Secret) credential).getChars());

            Connection dbConnection = null;
            ResultSet rs = null;
            PreparedStatement prepStmt = null;
            String sql = null;
            dbConnection = this.getDBConnection();
            dbConnection.setAutoCommit(false);
            // get the SQL statement used to select user details
            sql = this.realmConfig.getUserStoreProperty(JDBCRealmConstants.SELECT_USER_NAME);
            if (log.isDebugEnabled()) {
                log.debug(sql);
            }

            prepStmt = dbConnection.prepareStatement(sql);
            prepStmt.setString(1, userName);
            // check whether tenant id is used
            if (sql.contains(UserCoreConstants.UM_TENANT_COLUMN)) {
                prepStmt.setInt(2, this.tenantId);
            }

            rs = prepStmt.executeQuery();
            if (rs.next()) {
                userID = rs.getString(1);
                String storedPassword = rs.getString(3);

                // check whether password is expired or not
                boolean requireChange = rs.getBoolean(5);
                Timestamp changedTime = rs.getTimestamp(6);
                GregorianCalendar gc = new GregorianCalendar();
                gc.add(GregorianCalendar.HOUR, -24);
                Date date = gc.getTime();
                if (!(requireChange && changedTime.before(date))) {
                    // compare the given password with stored password using jasypt
                    isAuthenticated = passwordEncryptor.checkPassword(candidatePassword, storedPassword);
                }
            }
            dbConnection.commit();
            log.info(userName + " is authenticated? " + isAuthenticated);
        } catch (SQLException exp) {
            try {
                this.getDBConnection().rollback();
            } catch (SQLException e1) {
                throw new UserStoreException("Transaction rollback connection error occurred while" +
                        " retrieving user authentication info. Authentication Failure.", e1);
            }
            log.error("Error occurred while retrieving user authentication info.", exp);
            throw new UserStoreException("Authentication Failure");
        }
        if (isAuthenticated) {
            user = getUser(userID, userName);
            AuthenticationResult authenticationResult = new AuthenticationResult(
                    AuthenticationResult.AuthenticationStatus.SUCCESS);
            authenticationResult.setAuthenticatedUser(user);
            return authenticationResult;
        } else {
            AuthenticationResult authenticationResult = new AuthenticationResult(
                    AuthenticationResult.AuthenticationStatus.FAIL);
            authenticationResult.setFailureReason(new FailureReason("Invalid credentials."));
            return authenticationResult;
        }
    }

    @Override
    protected String preparePassword(Object password, String saltValue) throws UserStoreException {
        if (password != null) {
            String candidatePassword = String.copyValueOf(((Secret) password).getChars());
            // ignore saltValue for the time being
            log.info("Generating hash value using jasypt...");
            return passwordEncryptor.encryptPassword(candidatePassword);
        } else {
            log.error("Password cannot be null");
            throw new UserStoreException("Authentication Failure");
        }
    }

    private AuthenticationResult getAuthenticationResult(String reason) {

        AuthenticationResult authenticationResult = new AuthenticationResult(
                AuthenticationResult.AuthenticationStatus.FAIL);
        authenticationResult.setFailureReason(new FailureReason(reason));
        return authenticationResult;
    }
}

Step 3: Register as an OSGi Service

After overriding the doAuthenticateWithUserName and preparePassword methods in our CustomUserStoreManager class, we need to register it as an OSGi service so that we can plug it into the WSO2 Identity Server. To do that, we need to create a new package named internal and create a new class there.

package org.wso2.custom.userstore.manager.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.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferenceCardinality;
import org.osgi.service.component.annotations.ReferencePolicy;
import org.wso2.carbon.user.api.UserStoreManager;
import org.wso2.carbon.user.core.service.RealmService;
import org.wso2.custom.userstore.manager.CustomUserStoreManager;

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

    @Activate
    protected void activate(ComponentContext ctxt) {

        UserStoreManager customUserStoreManager = new CustomUserStoreManager();
        ctxt.getBundleContext().registerService(UserStoreManager.class.getName(),
                customUserStoreManager, null);
        log.info("CustomUserStoreManager bundle activated successfully..");
    }

    @Deactivate
    protected void deactivate(ComponentContext ctxt) {

        if (log.isDebugEnabled()) {
            log.debug("Custom User Store Manager is deactivated ");
        }
    }

    @Reference(
            name = "RealmService",
            service = org.wso2.carbon.user.core.service.RealmService.class,
            cardinality = ReferenceCardinality.MANDATORY,
            policy = ReferencePolicy.DYNAMIC,
            unbind = "unsetRealmService")

    protected void setRealmService(RealmService rlmService) {

        realmService = rlmService;
    }

    protected void unsetRealmService(RealmService realmService) {

        realmService = null;
    }
}

Since the above code is highly dependent on OSGi microservices, I am not going to explain it here. But, if you want to know why we needed to write a class like this, you can check my article on OSGi microservices from here.

Step 4: Build and Test the Custom User Store Manager

Now, we can build our custom user store manager by executing the following command.

mvn clean install -DskipTests

After that, you can find the org.wso2.custom.userstore.manager-1.0-SNAPSHOT.jar file in the target directory of the project.

Download a fresh pack of WSO2 Identity Server 6.1 from here and go to <IS_HOME>/repository/components/dropins and paste the org.wso2.custom.userstore.manager-1.0-SNAPSHOT.jar file there. Next, go to <IS_HOME>/repository/conf directory, open deployment.toml file and paste the following configuration there.

[user_store_mgt]
custom_user_stores=["org.wso2.custom.userstore.manager.CustomUserStoreManager"]

Now we can start the WSO2 Identity Server by using the following commands.

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

  • Windows → wso2server.bat -DosgiConsole

With -DosgiConsole flag, you are activating the inbuilt OSGi console of the WSO2 Identity Server. With ss <component_name>command you can check whether your custom component is in theACTIVE state or not. You can check the other important OSGi Console commands from this article.

If the component is not in the ACTIVE state, you can use b <component_id> and diag <component_id> to resolve issues with the component.

Now, type https://localhost:9443/carbon and go to the WSO2 Management Console. Use the default username admin and default password admin to log in. Next, go to, User Stores → Add.

From the drop-down menu of User Store Manager Class you can find our custom user store manager. Since we have created this by extending UniqueIDJDBCUserStoreManager class, we can provide a JDBC connection configuration and create a new user store.

So this is it! This is how you can create a custom user store for the WSO2 Identity Server. If you want to find more information regarding user stores and how to write custom user stores, you can find the necessary information from the official documentation.

You can find the code of the implemented custom user store manager from here.

https://github.com/nipunaupeksha/wso2-custom-components-medium/tree/main/org.wso2.custom.userstore.manager

Â