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.
Conclusion
Now that we have walked you through the process of configuring a custom user store for WSO2 Identity Server, we hope that you understand the essential concepts and the importance of identity management and user authentication. Through this guide, you will now know how to create and manage JDBC-based user stores (both default and custom) using the WSO2 Identity Server and implement it for your specific use case.
For expert guidance surrounding the WSO2 Identity Server, we encourage you to reach out to us if you have any questions or require further assistance. Our team at Chakray is always willing to extend our support so that you can carry on your path to seamless and secure authentication!