WSO2

How to Configure a Custom User Store in WSO2 Identity Server 5.7.0 | WSO2 IS Tutorial

This post explains step-by-step how to configure a custom user store for WSO2 Identity Server 5.7.0.  

To better understand this, we will first define what it is and then review the following use case:  A company that already has a user database and needs to login to the Identity Server through said database.

What is a User Store?

A user store is a data repository where user information and functions are saved, including, among many others, login name, password, name and surname and e-mail address.

By default WSO2 Identity Server comes pre-configured with the 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

Can be checked from the product console (http://dominio:9443/carbon → admi/admin user)

Going to Main → Identity → User Stores → Add.

WSO2 Identity Server supports LDAP, AD and JDBC-based user stores. In most cases, the custom user store refers to a JDBC-based user store  with a different pattern.

WSO2 Identity Server provides a JDBC-based user store with a default pattern (its own pattern). If a company does not have a user store or needs a new store, then the default pattern can be used without modification. However in most cases, the company already has an existing user store and wants to connect it to the WSO2 Identity Server for Single Sing-On configuration.

How to authenticate a company’s user database in Identity Server through said database. 

For the example (use case) the following table named USERS should be created using the following script:

CREATE TABLE USERS (
    ID INT NOT NULL PRIMARY KEY,
    USERNAME VARCHAR (100),
    PASSWORD VARCHAR (100),
    EMAIL VARCHAR (240)
);

INSERT INTO USERS (ID, USERNAME, PASSWORD, EMAIL) VALUES (1, ‘admin’, ‘admin’, ‘admin@chakray.com’);
INSERT INTO USERS (ID, USERNAME, PASSWORD, EMAIL) VALUES (2, ‘user1’, ‘user1’, ‘user1@chakray.com’);
INSERT INTO USERS (ID, USERNAME, PASSWORD, EMAIL) VALUES (3, ‘user2’, ‘user2’, ‘user2@chakray.com’);
INSERT INTO USERS (ID, USERNAME, PASSWORD, EMAIL) VALUES (4, ‘user3’, ‘user3’, ‘user3@chakray.com’);
INSERT INTO USERS (ID, USERNAME, PASSWORD, EMAIL) VALUES (5, ‘user4’, ‘user4’, ‘user4@chakray.com’);
INSERT INTO USERS (ID, USERNAME, PASSWORD, EMAIL) VALUES (6, ‘user5’, ‘user5’, ‘user5@chakray.com’);
INSERT INTO USERS (ID, USERNAME, PASSWORD, EMAIL) VALUES (7, ‘user6’, ‘user6’, ‘user6@chakray.com’);
INSERT INTO USERS (ID, USERNAME, PASSWORD, EMAIL) VALUES (8, ‘user7’, ‘user7’, ‘user7@chakray.com’);
INSERT INTO USERS (ID, USERNAME, PASSWORD, EMAIL) VALUES (9, ‘user8’, ‘user8’, ‘user8@chakray.com’);

The following tools are required:

  • IDE (Eclipse, developer-studio6.4.0)
  • Apache Maven
  • JDBC Driver for the Database where the USERS table is created

The subsequent steps to be followed are as listed below:

1.Generate the project in Maven from the IDE

2.Copy the JDBC Driver from your database to the [IS_HOME]/lib directory

3.Use the example code shown below for the pom.xml file

This file contains information about the project, sources, test, dependencies, plugins, version, etc.

<?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</groupId>
<artifactId>org.wso2.custom.user.store.manager</artifactId>
<version>1.0</version>
<packaging>bundle</packaging>
<repositories>
<repository>
<id>wso2-nexus</id>
<name>WSO2 internal Repository</name>
<url>http://maven.wso2.org/nexus/content/groups/wso2-public/</url>
<releases>
<enabled>true</enabled>
<updatePolicy>daily</updatePolicy>
<checksumPolicy>ignore</checksumPolicy>
</releases>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.wso2.carbon</groupId>
<artifactId>org.wso2.carbon.user.core</artifactId>
<version>4.4.11</version>
</dependency>
<dependency>
<groupId>org.wso2.carbon</groupId>
<artifactId>org.wso2.carbon.utils</artifactId>
<version>4.4.11</version>
</dependency>
<dependency>
<groupId>org.wso2.carbon</groupId>
<artifactId>org.wso2.carbon.user.api</artifactId>
<version>4.4.11</version>
</dependency>
<dependency>
<groupId>org.jasypt</groupId>
<artifactId>jasypt</artifactId>
<version>1.9.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.0</version>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-scr-plugin</artifactId>
<version>1.0.10</version>
<executions>
<execution>
<id>generate-scr-scrdescriptor</id>
<goals>
<goal>scr</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<version>1.4.0</version>
<extensions>true</extensions>
<configuration>
<instructions>
<Bundle-SymbolicName>${pom.artifactId}</Bundle-SymbolicName>
<Bundle-Name>${pom.artifactId}</Bundle-Name>
<Private-Package>
org.wso2.sample.user.store.manager.internal
</Private-Package>
<Export-Package>
!org.wso2.sample.user.store.manager.internal,
org.wso2.sample.user.store.manager.*,
</Export-Package>
<Import-Package>
javax.servlet; version=2.4.0,
javax.servlet.http; version=2.4.0,
org.wso2.carbon.base.*,
org.wso2.carbon.user.core.*,
<!--                            org.apache.lucene.*,-->
*;resolution:=optional
</Import-Package>
<DynamicImport-Package>*</DynamicImport-Package>
</instructions>
</configuration>
</plugin>
</plugins>
</build>
</project>

4.After that, we have to write the custom JDBCUserStoreManager to be compatible with the previous pattern. Therefore, a class extending from org.wso2.carbon.user.core.jdbc.JDBCUserStoreManager must be created.  In this case, I have created the CustomUserStoreManager.java class

package org.wso2.custom.user.store.manager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jasypt.util.password.StrongPasswordEncryptor;
import org.wso2.carbon.CarbonConstants;
import org.wso2.carbon.user.api.Properties;
import org.wso2.carbon.utils.Secret;
import org.wso2.carbon.user.api.Property;
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.jdbc.JDBCRealmConstants;
import org.wso2.carbon.user.core.jdbc.JDBCUserStoreManager;
import org.wso2.carbon.user.core.profile.ProfileConfigurationManager;
import org.wso2.carbon.user.core.util.DatabaseUtil;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Date;
import java.util.Map;
public class CustomUserStoreManager extends JDBCUserStoreManager {
private static Log log = LogFactory.getLog(CustomUserStoreManager.class);
public CustomUserStoreManager() {
}
public CustomUserStoreManager(org.wso2.carbon.user.api.RealmConfiguration realmConfig,
Map<String, Object> properties,
ClaimManager claimManager,
ProfileConfigurationManager profileManager,
UserRealm realm, Integer tenantId)
throws UserStoreException {
super(realmConfig, properties, claimManager, profileManager, realm, tenantId, false);
}
@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.copyValueOf(((Secret) credential).getChars());
String storedPassword="";
boolean isAuthed = false;
try {
dbConnection = getDBConnection();
dbConnection.setAutoCommit(false);
//paring the SELECT_USER_SQL from user_mgt.xml
sqlstmt = realmConfig.getUserStoreProperty(JDBCRealmConstants.SELECT_USER);
if (log.isDebugEnabled()) {
log.debug(sqlstmt);
}
prepStmt = dbConnection.prepareStatement(sqlstmt);
prepStmt.setString(1, userName);
rs = prepStmt.executeQuery();
if (rs.next()) {
storedPassword = rs.getString("password");
if ((storedPassword != null) && (storedPassword.trim().equals(password))) {
isAuthed = true;
}
}
} catch (SQLException e) {
throw new UserStoreException("Authentication Failure. Using sql :" + sqlstmt + " " + password + " " + storedPassword);
} finally {
DatabaseUtil.closeAllConnections(dbConnection, rs, prepStmt);
}
if (log.isDebugEnabled()) {
log.debug("User " + userName + " login attempt. Login success :: " + isAuthed);
}
return isAuthed;
}
@Override
public Date getPasswordExpirationTime(String userName) throws UserStoreException {
return null;
}
protected boolean isValueExisting(String sqlStmt, Connection dbConnection, Object... params)
throws UserStoreException {
PreparedStatement prepStmt = null;
ResultSet rs = null;
boolean isExisting = false;
boolean doClose = false;
try {
if (dbConnection == null) {
dbConnection = getDBConnection();
doClose = true; //because we created it
}
if (DatabaseUtil.getStringValuesFromDatabase(dbConnection, sqlStmt, params).length > 0) {
isExisting = true;
}
return isExisting;
} catch (SQLException e) {
log.error(e.getMessage(), e);
log.error("Using sql : " + sqlStmt);
throw new UserStoreException(e.getMessage(), e);
} finally {
if (doClose) {
DatabaseUtil.closeAllConnections(dbConnection, rs, prepStmt);
}
}
}
public String[] getUserListFromProperties(String property, String value, String profileName)
throws UserStoreException {
return new String[0];
}
/*@Override
public Map<String, String> doGetUserClaimValues(String userName, String[] claims,
String domainName) throws UserStoreException {
return new HashMap<String, String>();
}*/
/*@Override
public String doGetUserClaimValue(String userName, String claim, String profileName)
throws UserStoreException {
return null;
}*/
@Override
public boolean isReadOnly() throws UserStoreException {
return true;
}
@Override
public void doAddUser(String userName, Object credential, String[] roleList,
Map<String, String> claims, String profileName,
boolean requirePasswordChange) throws UserStoreException {
throw new UserStoreException(
"User store is operating in read only mode. Cannot write into the user store.");
}
public void doAddRole(String roleName, String[] userList, org.wso2.carbon.user.api.Permission[] permissions)
throws UserStoreException {
throw new UserStoreException(
"User store is operating in read only mode. Cannot write into the user store.");
}
@Override
public void doDeleteRole(String roleName) throws UserStoreException {
throw new UserStoreException(
"User store is operating in read only mode. Cannot write into the user store.");
}
@Override
public void doDeleteUser(String userName) throws UserStoreException {
throw new UserStoreException(
"User store is operating in read only mode. Cannot write into the user store.");
}
@Override
public boolean isBulkImportSupported() {
return false;
}
@Override
public void doUpdateRoleName(String roleName, String newRoleName) throws UserStoreException {
throw new UserStoreException(
"User store is operating in read only mode. Cannot write into the user store.");
}
@Override
public void doUpdateUserListOfRole(String roleName, String[] deletedUsers, String[] newUsers)
throws UserStoreException {
throw new UserStoreException(
"User store is operating in read only mode. Cannot write into the user store.");
}
@Override
public void doUpdateRoleListOfUser(String userName, String[] deletedRoles, String[] newRoles)
throws UserStoreException {
throw new UserStoreException(
"User store is operating in read only mode. Cannot write into the user store.");
}
@Override
public void doSetUserClaimValue(String userName, String claimURI, String claimValue,
String profileName) throws UserStoreException {
throw new UserStoreException(
"User store is operating in read only mode. Cannot write into the user store.");
}
@Override
public void doSetUserClaimValues(String userName, Map<String, String> claims,
String profileName) throws UserStoreException {
throw new UserStoreException(
"User store is operating in read only mode. Cannot write into the user store.");
}
@Override
public void doDeleteUserClaimValue(String userName, String claimURI, String profileName)
throws UserStoreException {
throw new UserStoreException(
"User store is operating in read only mode. Cannot write into the user store.");
}
@Override
public void doDeleteUserClaimValues(String userName, String[] claims, String profileName)
throws UserStoreException {
throw new UserStoreException(
"User store is operating in read only mode. Cannot write into the user store.");
}
@Override
public void doUpdateCredential(String userName, Object newCredential, Object oldCredential)
throws UserStoreException {
throw new UserStoreException(
"User store is operating in read only mode. Cannot write into the user store.");
}
@Override
public void doUpdateCredentialByAdmin(String userName, Object newCredential)
throws UserStoreException {
throw new UserStoreException(
"User store is operating in read only mode. Cannot write into the user store.");
}
public String[] getExternalRoleListOfUser(String userName) throws UserStoreException {
return new String[0];
}
@Override
public String[] doGetRoleNames(String filter, int maxItemLimit) throws UserStoreException {
return new String[0];
}
@Override
public boolean doCheckExistingRole(String roleName) throws UserStoreException {
return false;
}
@Override
public boolean doCheckExistingUser(String userName) throws UserStoreException {
return true;
}
@Override
public org.wso2.carbon.user.api.Properties getDefaultUserStoreProperties(){
Properties properties = new Properties();
properties.setMandatoryProperties(CustomUserStoreConstants.CUSTOM_UM_MANDATORY_PROPERTIES.toArray
(new Property[CustomUserStoreConstants.CUSTOM_UM_MANDATORY_PROPERTIES.size()]));
properties.setOptionalProperties(CustomUserStoreConstants.CUSTOM_UM_OPTIONAL_PROPERTIES.toArray
(new Property[CustomUserStoreConstants.CUSTOM_UM_OPTIONAL_PROPERTIES.size()]));
properties.setAdvancedProperties(CustomUserStoreConstants.CUSTOM_UM_ADVANCED_PROPERTIES.toArray
(new Property[CustomUserStoreConstants.CUSTOM_UM_ADVANCED_PROPERTIES.size()]));
return properties;
}
}

5.In the CustomUserStoreConstants.java class you can set the mandatory, optional, and advanced configuration as follows:

NOTE: Here you must fill in the corresponding values to access the database where you created the USERS table (e.g. “Driver Name, URL, user, password”).

package org.wso2.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 CustomUserStoreConstants {
public static final ArrayList<Property> CUSTOM_UM_MANDATORY_PROPERTIES = new ArrayList<Property>();
public static final ArrayList<Property> CUSTOM_UM_OPTIONAL_PROPERTIES = new ArrayList<Property>();
public static final ArrayList<Property> CUSTOM_UM_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);
//Advanced Properties
setAdvancedProperty("SelectUserSQL","Select User SQL", "SELECT * FROM USERS WHERE USERNAME=?", "");
setAdvancedProperty("UserFilterSQL","User Filter SQL", "SELECT USERNAME FROM USERS WHERE USERNAME LIKE ? ORDER BY ID", "");
}
private static void setProperty(String name, String displayName, String value, String description) {
Property property = new Property(name, value, displayName + "#" +description, null);
CUSTOM_UM_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);
CUSTOM_UM_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);
CUSTOM_UM_ADVANCED_PROPERTIES.add(property);
}
}

6.Class for entering Custom User Store Manager into OSGI framework  

package org.wso2.custom.user.store.manager.internal;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.osgi.service.component.ComponentContext;
import org.wso2.carbon.user.api.UserStoreManager;
import org.wso2.carbon.user.core.service.RealmService;
import org.wso2.custom.user.store.manager.CustomUserStoreManager;
/**
* @scr.component name="custom.user.store.manager.dscomponent" immediate=true
* @scr.reference name="user.realmservice.default"
* interface="org.wso2.carbon.user.core.service.RealmService"
* cardinality="1..1" policy="dynamic" bind="setRealmService"
* unbind="unsetRealmService"
*/
public class CustomUserStoreMgtDSComponent {
private static Log log = LogFactory.getLog(CustomUserStoreMgtDSComponent.class);
private static RealmService realmService;
protected void activate(ComponentContext ctxt) {
CustomUserStoreManager customUserStoreManager = new CustomUserStoreManager();
ctxt.getBundleContext().registerService(UserStoreManager.class.getName(), customUserStoreManager, null);
log.info("CustomUserStoreManager bundle activated successfully..");
}
protected void deactivate(ComponentContext ctxt) {
if (log.isDebugEnabled()) {
log.debug("Custom User Store Manager is deactivated ");
}
}
protected void setRealmService(RealmService rlmService) {
realmService = rlmService;
}
protected void unsetRealmService(RealmService realmService) {
realmService = null;
}
}

7.Compile the custom user store code to generate the file: org.wso2.custom.user.store.manager-1.0.jar   

8.Then copy the org.wso2.custom.user.store.manager-1.0.jar file into the folder <IS_HOME>/repository/components/dropins

9.Finally, restart the WSO2 IS service.

10.Once restarted, log in to the administration console.

11.Click Add option in User Stores section.

12.In the User Store Manager Class drop-down list you will see that the class org.wso2.custom.user.store.manager.CustomUserStoreManager has been added.

13.Select the class org.wso2.custom.user.store.manager.CustomUserStoreManager

14.Fill in the connection data to the previously created DB.

15.In the following window the user store created will be shown.

16.To validate that the users have been added, click on the option List of Section Users.

Conclusions:

An extension has been configured in Identity Server to set up a custom user group, which comprises a table of users in an external database.

This will allow us to perform authentication tasks on said data through the WSO2 Identity Manager services.

Are the users from your company protected?

Learn about Access-control security and anticipate to identity theft.

Written By

Juan Carlos Montes

WSO2 Specialist