Tuesday, March 8, 2016

How to Write a Custom User Store Manager - WSO2 Identity Server 5.1.0

WSO2 Identity Sever OOTB support following user stores.
  • org.wso2.carbon.user.core.jdbc.JDBCUserStoreManager
  • org.wso2.carbon.user.core.ldap.ReadOnlyLDAPUserStoreManager
  • org.wso2.carbon.user.core.ldap.ReadWriteLDAPUserStoreManager
  • org.wso2.carbon.user.core.ldap.ActiveDirectoryLDAPUserStoreManager
  • org.wso2.carbon.identity.user.store.remote.CarbonRemoteUserStoreManger
There are some cases, we have to write a custom implementation. Here I am explaining an step by step guide of how to write a custom users store manager for Identity Server 5.1.0. 

Requirement: A company already has a user database and need to authenticate to Identity Server through that database.

Following is the schema of USER database.


CREATE TABLE TEST_USER (
 USER_ID INT NOT NULL PRIMARY KEY,
    USER_NAME VARCHAR(100),
    ENCRYPTED_USER_PASSWORD VARCHAR(100),
    EMAIL_ADDRESS VARCHAR(240),
    EMPLOYEE_ID INT
);

INSERT INTO TEST_USER (USER_ID, USER_NAME, ENCRYPTED_USER_PASSWORD, EMAIL_ADDRESS, EMPLOYEE_ID) VALUES (1, "testadmin", "testpass", "admin@act.org", 1000);
INSERT INTO TEST_USER (USER_ID, USER_NAME, ENCRYPTED_USER_PASSWORD, EMAIL_ADDRESS, EMPLOYEE_ID) VALUES (2, "user1", "user1", "user1@act.org", 1001);
INSERT INTO TEST_USER (USER_ID, USER_NAME, ENCRYPTED_USER_PASSWORD, EMAIL_ADDRESS, EMPLOYEE_ID) VALUES (3, "user2", "user2", "user2@act.org", 1002);
INSERT INTO TEST_USER (USER_ID, USER_NAME, ENCRYPTED_USER_PASSWORD, EMAIL_ADDRESS, EMPLOYEE_ID) VALUES (4, "user3", "user3", "user3@act.org", 1003);
INSERT INTO TEST_USER (USER_ID, USER_NAME, ENCRYPTED_USER_PASSWORD, EMAIL_ADDRESS, EMPLOYEE_ID) VALUES (5, "user4", "user4", "user4@act.org", 1004);
INSERT INTO TEST_USER (USER_ID, USER_NAME, ENCRYPTED_USER_PASSWORD, EMAIL_ADDRESS, EMPLOYEE_ID) VALUES (6, "user5", "user5", "user5@act.org", 1005);
INSERT INTO TEST_USER (USER_ID, USER_NAME, ENCRYPTED_USER_PASSWORD, EMAIL_ADDRESS, EMPLOYEE_ID) VALUES (7, "user6", "user6", "user6@act.org", 1006);
INSERT INTO TEST_USER (USER_ID, USER_NAME, ENCRYPTED_USER_PASSWORD, EMAIL_ADDRESS, EMPLOYEE_ID) VALUES (8, "user7", "user7", "user7@act.org", 1007);
INSERT INTO TEST_USER (USER_ID, USER_NAME, ENCRYPTED_USER_PASSWORD, EMAIL_ADDRESS, EMPLOYEE_ID) VALUES (9, "user8", "user8", "user8@act.org", 1008);


Writing Custom User Store Manager
  • You can find the sample code from here
  • Our custom User store is a jdbc based user store and we can write it by extending the org.wso2.carbon.user.core.jdbc.JDBCUserStoreManager.
  • We need to override the doAuthenticate method to authenticate using the new database. 


 @Override
    public boolean doAuthenticate(String userName, Object credential) throws UserStoreException {

        if (CarbonConstants.REGISTRY_ANONNYMOUS_USERNAME.equals(userName)) {
            log.error("Anonymous user trying to login");
            return false;
        }

        Connection dbConnection = null;
        ResultSet rs = null;
        PreparedStatement prepStmt = null;
        String sqlstmt = null;
        String password = (String) credential;
        boolean isAuthed = false;

        try {
            dbConnection = getDBConnection();
            dbConnection.setAutoCommit(false);
            sqlstmt = realmConfig.getUserStoreProperty(JDBCRealmConstants.SELECT_USER);

            prepStmt = dbConnection.prepareStatement(sqlstmt);
            prepStmt.setString(1, userName);

            rs = prepStmt.executeQuery();

            if (rs.next()) {
                String storedPassword = rs.getString("ENCRYPTED_USER_PASSWORD");
                if ((storedPassword != null) && (storedPassword.trim().equals(password))) {
                    isAuthed = true;
                }
            }
        } catch (SQLException e) {
            throw new UserStoreException("Authentication Failure. Using sql :" + sqlstmt);
        } finally {
            DatabaseUtil.closeAllConnections(dbConnection, rs, prepStmt);
        }

        if (log.isDebugEnabled()) {
            log.debug("User " + userName + " login attempt. Login success :: " + isAuthed);
        }

        return isAuthed;
    }
  • We have to define custom SQL queries and we can make them as configurable by overriding getDefaultUserStoreProperties method as follows.

  @Override
    public org.wso2.carbon.user.api.Properties getDefaultUserStoreProperties() {
        Properties properties = new Properties();
        properties.setMandatoryProperties(CustomUserStoreManagerConstants.MANDATORY_PROPERTIES.toArray
                (new Property[CustomUserStoreManagerConstants.MANDATORY_PROPERTIES.size()]));
        properties.setOptionalProperties(CustomUserStoreManagerConstants.OPTIONAL_PROPERTIES.toArray
                (new Property[CustomUserStoreManagerConstants.OPTIONAL_PROPERTIES.size()]));
        properties.setAdvancedProperties(CustomUserStoreManagerConstants.ADVANCED_PROPERTIES.toArray
                (new Property[CustomUserStoreManagerConstants.ADVANCED_PROPERTIES.size()]));
        return properties;
    }


  • We can set the mandatory, optional and advanced configuration as follows. 

package com.wso2.carbon.custom.user.store.manager;

import org.wso2.carbon.user.api.Property;
import org.wso2.carbon.user.core.UserStoreConfigConstants;
import org.wso2.carbon.user.core.jdbc.JDBCRealmConstants;

import java.util.ArrayList;

public class CustomUserStoreManagerConstants {

    public static final ArrayList<Property> MANDATORY_PROPERTIES = new ArrayList<Property>();
    public static final ArrayList<Property> OPTIONAL_PROPERTIES = new ArrayList<Property>();
    public static final ArrayList<Property> ADVANCED_PROPERTIES = new ArrayList<Property>();

    static {
        setMandatoryProperty(JDBCRealmConstants.DRIVER_NAME, "Driver Name", "", "Full qualified driver name");
        setMandatoryProperty(JDBCRealmConstants.URL, "Connection URL", "", "URL of the user store database");
        setMandatoryProperty(JDBCRealmConstants.USER_NAME, "User Name", "", "Username for the database");
        setMandatoryProperty(JDBCRealmConstants.PASSWORD, "Password", "", "Password for the database");

        setProperty(UserStoreConfigConstants.disabled, "Disabled", "false", UserStoreConfigConstants.disabledDescription);
        setProperty("ReadOnly", "Read Only", "true", "Indicates whether the user store of this realm operates in the user read only mode or not");
        setProperty(UserStoreConfigConstants.SCIMEnabled, "SCIM Enabled", "false", UserStoreConfigConstants.SCIMEnabledDescription);

        setAdvancedProperty("SelectUserSQL", "Select User SQL", "SELECT * FROM TEST_USER WHERE USER_NAME=?", "");
        setAdvancedProperty("UserFilterSQL", "User Filter SQL", "SELECT USER_NAME FROM TEST_USER WHERE USER_NAME LIKE" +
                " ? ORDER BY USER_NAME", "");
        setAdvancedProperty("IsUserExistingSQL", "Is User Existing SQL", "SELECT USER_NAME FROM TEST_USER WHERE " +
                "USER_NAME=? ", "");
    }

    private static void setProperty(String name, String displayName, String value, String description) {
        Property property = new Property(name, value, displayName + "#" + description, null);
        OPTIONAL_PROPERTIES.add(property);
    }

    private static void setMandatoryProperty(String name, String displayName, String value, String description) {
        Property property = new Property(name, value, displayName + "#" + description, null);
        MANDATORY_PROPERTIES.add(property);
    }

    private static void setAdvancedProperty(String name, String displayName, String value, String description) {
        Property property = new Property(name, value, displayName + "#" + description, null);
        ADVANCED_PROPERTIES.add(property);
    }
}



  • Register Custom User Store Manager in OSGI framework

/**
 * @scr.component name="com.wso2.carbon.custom.user.store.manager.component" immediate="true"
 * @scr.reference name="realm.service"
 * interface="org.wso2.carbon.user.core.service.RealmService"cardinality="1..1"
 * policy="dynamic" bind="setRealmService" unbind="unsetRealmService"
 */
public class CustomUserStoreManagerServiceComponent {

    private static Log log = LogFactory.getLog(CustomUserStoreManagerServiceComponent.class);
    
    private static RealmService realmService;
    
    protected void activate(ComponentContext ctxt) {
     Hashtable<String, String> props = new Hashtable<String, String>();       
        CustomUserStoreManager customUserStoreManager = new CustomUserStoreManager();
        ctxt.getBundleContext().registerService(UserStoreManager.class.getName(), customUserStoreManager, props);
        log.info("CustomUserStoreManager bundle activated successfully..");
    }

    protected void deactivate(ComponentContext ctxt) {
        if (log.isDebugEnabled()) {
            log.info("CustomUserStoreManager bundle is deactivated");
        }
    }
    
    protected void setRealmService(RealmService realmService) {
        log.debug("Setting the Realm Service");
        CustomUserStoreManagerServiceComponent.realmService = realmService;
    }

    protected void unsetRealmService(RealmService realmService) {
        log.debug("UnSetting the Realm Service");
        CustomUserStoreManagerServiceComponent.realmService = null;
    }

    public static RealmService getRealmService() {
        return realmService;
    }
}


Identity Server Configurations. 


  • Compile the custom user store manager code and then you will get com.wso2.carbon.custom.user.store.manager-1.0.0.jar OSGI bundlle.
  • Copy com.wso2.carbon.custom.user.store.manager-1.0.0.jar into <IS_HOME>/repository/components/dropins folder.
  • Configure new database in <IS_HOME>//repository/conf/datasources/master-datasources.xml file as follows

<datasource>
    <name>CustomUserDB</name>
    <description>Custom User Database</description>
    <jndiConfig>
        <name>jdbc/CustomUserDB</name>
    </jndiConfig>
    <definition type="RDBMS">
        <configuration>
            <url>jdbc:mysql://localhost:3306/Custom</url>
            <username>root</username>
            <password>root</password>
            <driverClassName>com.mysql.jdbc.Driver</driverClassName>
            <maxActive>50</maxActive>
            <maxWait>60000</maxWait>
            <testOnBorrow>true</testOnBorrow>
            <validationQuery>SELECT 1</validationQuery>
            <validationInterval>30000</validationInterval>
        </configuration>
    </definition>
</datasource>



  • Start Identity Server.
  • Go to Add User button inside Home/Identity.




  • Then you can fill the configurations as above image
  • Then assign login permission to Internal/everyone role 
  • Try to login to management console using user1/user1