Saturday, September 21, 2013

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

With this post I will be demonstrating writing a simple custom user store manager for WSO2 Carbon and specifically in WSO2 Identity Server 4.5.0 which is released recently. The Content is as follows,
  1. Use case
  2. Writing the custom User Store Manager
  3. Configuration in Identity Server
You can download the sample here.

Use Case

By default WSO2 Carbon has four implementations of User Store Managers as follows.
  • 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
Let's look at a scenario where a company has a simple user store where they have kept customer_id, customer_name and the password (For the moment let's not worry about salting etc. as purpose is to demonstrate getting a custom user store into action). So the company may want to keep this as it is, as there may be other services depending on this and still wanting to have identities managed. Obviously it's not a good practice to duplicate these sensitive data to another database to be used by the Identity Server as then the cost of securing both databases is high and can guide to conflicts. That is where a custom User Store Manager comes handy, with the high extendibility of Carbon platform.

So this is the scenario I am to demonstrate with only basic authentication.

We have the following user store which is currently in use at the company.
CREATE TABLE CUSTOMER_DATA (
             CUSTOMER_ID INTEGER NOT NULL AUTO_INCREMENT,
             CUSTOMER_NAME VARCHAR(255) NOT NULL,
             PASSWORD VARCHAR(255) NOT NULL,
             PRIMARY KEY (CUSTOMER_ID),
             UNIQUE(CUSTOMER_NAME)
);


INSERT INTO CUSTOMER_DATA (CUSTOMER_NAME, PASSWORD) VALUES("pushpalanka" ,"pushpalanka");
INSERT INTO CUSTOMER_DATA (CUSTOMER_NAME, PASSWORD) VALUES("lanka" ,"lanka"); 

I have only two entries in user store. :) Now what we want is to let these already available users to be visible to Identity Server, nothing less, nothing more. So it's only the basic authentication that User Store Manager should support, according to this scenario.

Writing the custom User Store Manager

There are just 3 things to adhere when we writing the User Store Manager and the rest will be done for us.

  • Implement the 'org.wso2.carbon.user.api.UserStoreManager' interface
There are several other options to do this, by implementing 'org.wso2.carbon.user.core.UserStoreManager' interface or extending 'org.wso2.carbon.user.core.common.AbstractUserStoreManager' class, as appropriate. In this case as we are dealing with a JDBC User Store, best option is to extend the existing JDBCUserStoreManager class and override the methods as required.
CustomUserStoreManager extends JDBCUserStoreManager 

@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("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;

    }

  • Register Custom User Store Manager in OSGI framework
This is just simple step to make sure new custom user store manager is available through OSGI framework. With this step the configuration of new user store manager becomes so easy with the UI in later steps. We just need to place following class inside the project.

/**
 * @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;
    }
}


  • Define the Properties Required for the User Store Manager
There needs to be this method 'getDefaultProperties()' as follows. The required properties are mentioned in the class 'CustomUserStoreConstants'. In the downloaded sample it can be clearly seen how this is used.
@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;
    } 

The advanced properties carries the required SQL statements for the user store, written according to the custom schema of our user store.
Now all set to go. You can build the project with your customization to the sample project or just use the jar in the target. Drop the jar inside CARBON_HOME/repository/components/dropins and drop mysql-connector-java-<>.jar inside CARBON_HOME/repository/components/lib. Start the server with ./wso2carbon.sh from CARBON_HOME/bin. In the start-up logs you will see following log printed.

INFO {org.wso2.sample.user.store.manager.internal.CustomUserStoreMgtDSComponent} -  CustomUserStoreManager bundle activated successfully.

Configuration in Identity Server

In the management console try to add a new user store as follows.
 In the shown space we will see our custom user store manager given as an options to use as the implementation class, as we registered this before in OSGI framework. Select it and fill the properties according to the user store.


Also in the property space we will now see the properties we defined in the constants class as below.
If our schema changes at any time we can edit it here in dynamic manner. Once finished we will have to wait a moment and after refreshing we will see the newly added user store domain, here I have named it 'wso2.com'. 
So let's verify whether the user are there. Go to 'Users and Roles' and in Users table we will now see the users details who were there in the custom user store as below.

 If we check the roles these users are assigned to Internal/everyone role. Modify the role permission to have 'login' allowed. Now if any of the above two users tried to login with correct credentials they are allowed.
So we have successfully configured Identity Server to use our Custom User Store without much hassel.

Cheers!

Ref: http://malalanayake.wordpress.com/2013/04/03/how-to-write-custom-jdbc-user-store-manager-with-wso2is-4-1-1-alpha/

Note: For the updated sample for Identity Server - 5.0.0, please use the link, https://svn.wso2.org/repos/wso2/people/pushpalanka/SampleCustomeUserStoreManager-5.0.0/

Google

14 comments :

  1. Hi Pushpalanka, thank you for your blog.
    I tried out your CustomUserStoreManager with WSO2IS 4.5.0, it works :), however I experienced some troubles when requesting tokens using "password" grant type (using OAuth 2.0):

    (1) Getting access token for the first time works fine, I get access token, refresh token, etc.
    (2) Subsequent requests for access token work fine too, I get tokens from (1), etc.
    (3) Subsequent request for refreshing tokens works fine too, I get new access and new refresh token, etc.
    (4) But now, subsequent request for access token results in getting tokens from the very first request (1) not the ones obtained in step (3) and validation of this access token fails.

    All subsequent requests for refreshing token do actually nothing. This happens when some user from secondary user store manager is used. When I repeat these steps using admin/admin as username/password which is kept as primary user, everything works as I expect - request for access token (4) returns new fresh tokens (from (3))

    I don't know whether I'm missing something, please it would be very helpful if you could look at it. I hope I wrote my problem clear for understanding :) Thank you in advance

    ReplyDelete
    Replies
    1. Hi Martin,

      Yes, I could understand the problem. At first sight it looked like a problem in caching. But as it only occurs with secondary user store there seems to be something else. I'll dig more into this and let you know. If you can share the configuration of your secondary user store without the sensitive details, it would be helpful (My-email: lankalux@gmail.com).

      Regards,

      Delete
    2. Hi Pushpalanka,

      Did you have a chance to look at the issue Martin reported. We are thinking of implementing a secondary user store for oAuth protected resources.

      Thanks
      Pradeep

      Delete
  2. Excellent post. Really really helpful.

    ReplyDelete
  3. If you came across an error in Windows environment caused by,
    length of the index "REG_PATH_IND_BY_PATH_VALUE" in line 31 of mysql.sql script it too long

    At creating the database use the following command,

    CREATE DATABASE JDBC_demo_user_store CHARACTER SET latin1;

    The error occuurs because of the key length is too high when mysql uses UTF-8 encoding which is Windows default. The default encoding for mysql in linux environment is latin1.



    ReplyDelete
  4. This comment has been removed by a blog administrator.

    ReplyDelete
  5. Hi, i tried to implements org.wso2.carbon.user.api.UserStoreManager instead of extending JDBCUserStoreManager but i get a class cast exception : java.lang.ClassCastException: org.wso2.sample.user.store.manager.CustomUserStoreManager cannot be cast to org.wso2.carbon.user.core.UserStoreManager.

    I don't know if i miss something?

    ReplyDelete
  6. Hi Pushpalanka,

    I am trying to configure the WSO@ version 4.1.0 with the custom DB (Oracle 11g XE). I just fallowed the steps that you mentioned, but getting the exception below. Could you please help me in resolving the below error.

    [2014-02-03 22:12:51,749] ERROR {org.wso2.carbon.ldap.server.DirectoryActivator} - An unknown exception occurred while starting LDAP server.
    [com.ctc.wstx.exc.WstxLazyException] com.ctc.wstx.exc.WstxIOException: Invalid UTF-8 start byte 0xa0 (at char #13386, byte #11999)
    at com.ctc.wstx.exc.WstxLazyException.throwLazily(WstxLazyException.java:45)
    at com.ctc.wstx.sr.StreamScanner.throwLazyError(StreamScanner.java:720)
    at com.ctc.wstx.sr.BasicStreamReader.safeFinishToken(BasicStreamReader.java:3643)
    at com.ctc.wstx.sr.BasicStreamReader.getText(BasicStreamReader.java:848)
    at org.apache.axiom.util.stax.wrapper.XMLStreamReaderWrapper.getText(XMLStreamReaderWrapper.java:164)
    at org.apache.axiom.om.impl.builder.StAXOMBuilder.createComment(StAXOMBuilder.java:476)
    at org.apache.axiom.om.impl.builder.StAXOMBuilder.next(StAXOMBuilder.java:279)
    at org.apache.axiom.om.impl.llom.OMElementImpl.buildNext(OMElementImpl.java:653)
    at org.apache.axiom.om.impl.llom.OMNodeImpl.getNextOMSibling(OMNodeImpl.java:122)
    at org.apache.axiom.om.impl.traverse.OMChildrenIterator.getNextNode(OMChildrenIterator.java:36)
    at org.apache.axiom.om.impl.traverse.OMAbstractIterator.hasNext(OMAbstractIterator.java:58)
    at org.apache.axiom.om.impl.traverse.OMFilterIterator.hasNext(OMFilterIterator.java:54)
    at org.apache.axiom.om.impl.llom.OMElementImpl.getFirstChildWithName(OMElementImpl.java:273)
    at org.wso2.carbon.user.core.config.RealmConfigXMLProcessor.buildRealmConfiguration(RealmConfigXMLProcessor.java:148)
    at org.wso2.carbon.ldap.server.configuration.LDAPConfigurationBuilder.getUserManagementXMLElement(LDAPConfigurationBuilder.java:560)
    at org.wso2.carbon.ldap.server.configuration.LDAPConfigurationBuilder.buildPartitionConfigurations(LDAPConfigurationBuilder.java:368)
    at org.wso2.carbon.ldap.server.configuration.LDAPConfigurationBuilder.buildConfigurations(LDAPConfigurationBuilder.java:179)
    at org.wso2.carbon.ldap.server.DirectoryActivator.start(DirectoryActivator.java:60)
    at org.eclipse.osgi.framework.internal.core.BundleContextImpl$1.run(BundleContextImpl.java:711)
    at java.security.AccessController.doPrivileged(Native Method)
    at org.eclipse.osgi.framework.internal.core.BundleContextImpl.startActivator(BundleContextImpl.java:702)
    at org.eclipse.osgi.framework.internal.core.BundleContextImpl.start(BundleContextImpl.java:683)
    at org.eclipse.osgi.framework.internal.core.BundleHost.startWorker(BundleHost.java:381)
    at org.eclipse.osgi.framework.internal.core.AbstractBundle.resume(AbstractBundle.java:389)
    at org.eclipse.osgi.framework.internal.core.Framework.resumeBundle(Framework.java:1130)
    at org.eclipse.osgi.framework.internal.core.StartLevelManager.resumeBundles(StartLevelManager.java:559)
    at org.eclipse.osgi.framework.internal.core.StartLevelManager.resumeBundles(StartLevelManager.java:544)
    at org.eclipse.osgi.framework.internal.core.StartLevelManager.incFWSL(StartLevelManager.java:457)
    at org.eclipse.osgi.framework.internal.core.StartLevelManager.doSetStartLevel(StartLevelManager.java:243)
    at org.eclipse.osgi.framework.internal.core.StartLevelManager.dispatchEvent(StartLevelManager.java:438)
    at org.eclipse.osgi.framework.internal.core.StartLevelManager.dispatchEvent(StartLevelManager.java:1)
    at org.eclipse.osgi.framework.eventmgr.EventManager.dispatchEvent(EventManager.java:230)
    at org.eclipse.osgi.framework.eventmgr.EventManager$EventThread.run(EventManager.java:340)


    Regards,
    Ravi

    ReplyDelete
    Replies
    1. Hi Ravindra,

      There is some invalid character in your user-mgt.xml.

      Delete
    2. Hi Pushpalanka, Yes you were right. There was some hidden junk characters in the xml file and i just removed those as well. But Still i am facing issues with it.

      TID: [0] [IS] [2014-02-11 12:51:06,232] ERROR {org.wso2.carbon.ldap.server.DirectoryActivator} - An unknown exception occurred while starting LDAP server. {org.wso2.carbon.ldap.server.DirectoryActivator}
      java.lang.NullPointerException: Name is null
      at java.lang.Enum.valueOf(Enum.java:235)

      A details logs are sent to your mail. Could you please guide me in resolving the issue.
      Thanks,
      Ravi

      Delete
  7. This comment has been removed by the author.

    ReplyDelete
  8. Hi Pushpamali,

    Download sample link is not working here. Could you kindly direct me to a correct link or can you send me the sample. My email
    suranga.herath@gmail.com

    Many Thanks,

    ReplyDelete
  9. Please find updated sample custom user store manager for WSO2 Identity Server at, https://svn.wso2.org/repos/wso2/people/pushpalanka/SampleCustomeUserStoreManager-5.0.0/

    ReplyDelete