Skip to main content

WSO2 Identity Server 5.2.0 - Setup Multiple Attribute login with JDBC userstore

In WSO2 Products multiple attribute login (login with either email or username for example) can be done with LDAP Userstore manager with simply by changing some configurations. But with JDBC Userstore manager we need some customization to achieve that. We can achieve that by using Implementing a custom userstore manager. In this blog entry I am going to make work with email and username. You can find the full sample here[1].


For login purposes


To login to the server with multiple attributes, you will need to override doAuthenticate method and doGetExternalRoleListOfUser method. Following are the overridden methods for login.


   @Override  
   public boolean doAuthenticate(String attribute, Object credential) throws UserStoreException {  
     if (!checkUserNameValid(attribute)) {  
       return false;  
     }  
     if (!checkUserPasswordValid(credential)) {  
       return false;  
     }  
     if (UserCoreUtil.isRegistryAnnonymousUser(attribute)) {  
       log.error("Anonnymous 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);  
       if (log.isDebugEnabled()) {  
         log.debug(sqlstmt);  
       }  
       prepStmt = dbConnection.prepareStatement(sqlstmt);  
       // Insert attribute as parameter for each occurrence of   
       int paramCount = StringUtils.countMatches(sqlstmt, "?");  
       // If we specify the tenant into query, we assume that it is the last parameter  
       if (sqlstmt.contains(UserCoreConstants.UM_TENANT_COLUMN)) {  
         // Assign attribute value to parameters, except the last one  
         for (int i = 1; i < paramCount; i++) {  
           prepStmt.setString(i, attribute);  
         }  
         prepStmt.setInt(paramCount, tenantId);  
       } else {  
         // There is no tenant indication, set all parameters with attribute value  
         for (int i = 1; i <= paramCount; i++) {  
           prepStmt.setString(i, attribute);  
         }  
       }  
       rs = prepStmt.executeQuery();  
       if (rs.next() == true) {  
         String storedPassword = rs.getString(3);  
         String saltValue = null;  
         if ("true".equalsIgnoreCase(realmConfig  
             .getUserStoreProperty(JDBCRealmConstants.STORE_SALTED_PASSWORDS))) {  
           saltValue = rs.getString(4);  
         }  
         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 == true && changedTime.before(date)) {  
           isAuthed = false;  
         } else {  
           password = this.preparePassword(password, saltValue);  
           if ((storedPassword != null) && (storedPassword.equals(password))) {  
             isAuthed = true;  
           }  
         }  
       }  
     } catch (SQLException e) {  
       String msg = "Error occurred while retrieving user authentication info.";  
       log.error(msg, e);  
       throw new UserStoreException("Authentication Failure");  
     } finally {  
       DatabaseUtil.closeAllConnections(dbConnection, rs, prepStmt);  
     }  
     if (log.isDebugEnabled()) {  
       log.debug("User " + attribute + " login attempt. Login success :: " + isAuthed);  
     }  
     return isAuthed;  
   }  
   @Override  
   public Date getPasswordExpirationTime(String attribute) throws UserStoreException {  
     Connection dbConnection = null;  
     ResultSet rs = null;  
     PreparedStatement prepStmt = null;  
     String sqlstmt;  
     Date date = null;  
     try {  
       dbConnection = getDBConnection();  
       dbConnection.setAutoCommit(false);  
       sqlstmt = realmConfig.getUserStoreProperty(JDBCRealmConstants.SELECT_USER);  
       if (log.isDebugEnabled()) {  
         log.debug(sqlstmt);  
       }  
       prepStmt = dbConnection.prepareStatement(sqlstmt);  
       // Insert attribute as parameter for each occurrence of   
       int paramCount = StringUtils.countMatches(sqlstmt, "?");  
       // If we specify the tenant into query, we assume that it is the last parameter  
       if (sqlstmt.contains(UserCoreConstants.UM_TENANT_COLUMN)) {  
         // Assign attribute value to parameters, except the last one  
         for (int i = 1; i < paramCount; i++) {  
           prepStmt.setString(i, attribute);  
         }  
         prepStmt.setInt(paramCount, tenantId);  
       } else {  
         // There is no tenant indication, set all parameters with attribute value  
         for (int i = 1; i <= paramCount; i++) {  
           prepStmt.setString(i, attribute);  
         }  
       }  
       rs = prepStmt.executeQuery();  
       if (rs.next() == true) {  
         boolean requireChange = rs.getBoolean(5);  
         Timestamp changedTime = rs.getTimestamp(6);  
         if (requireChange) {  
           GregorianCalendar gc = new GregorianCalendar();  
           gc.setTime(changedTime);  
           gc.add(GregorianCalendar.HOUR, 24);  
           date = gc.getTime();  
         }  
       }  
     } catch (SQLException e) {  
       String msg = "Error occurred while retrieving password expiration time.";  
       log.error(msg, e);  
       throw new UserStoreException(msg, e);  
     } finally {  
       DatabaseUtil.closeAllConnections(dbConnection, rs, prepStmt);  
     }  
     return date;  
   }  
   public String[] doGetExternalRoleListOfUser(String userName, String filter) throws UserStoreException {  
     if(log.isDebugEnabled()) {  
       log.debug("Getting roles of user: " + userName + " with filter: " + filter);  
     }  
     String sqlStmt;  
     if(this.isCaseSensitiveUsername()) {  
       sqlStmt = this.realmConfig.getUserStoreProperty("UserRoleSQL");  
     } else {  
       sqlStmt = this.realmConfig.getUserStoreProperty("UserRoleSQLCaseInsensitive");  
     }  
     ArrayList roles = new ArrayList();  
     if(sqlStmt == null) {  
       throw new UserStoreException("The sql statement for retrieving user roles is null");  
     } else {  
       String[] names;  
       if(sqlStmt.contains("UM_TENANT_ID")) {  
         names = this.getStringValuesFromDatabase(sqlStmt, new Object[]{userName, userName, Integer.valueOf(this.tenantId), Integer.valueOf(this.tenantId), Integer.valueOf(this.tenantId), Integer.valueOf(this.tenantId)});  
       } else {  
         names = this.getStringValuesFromDatabase(sqlStmt, new Object[]{userName});  
       }  
       if(log.isDebugEnabled()) {  
         if(names != null) {  
           String[] arr$ = names;  
           int len$ = names.length;  
           for(int i$ = 0; i$ < len$; ++i$) {  
             String name = arr$[i$];  
             log.debug("Found role: " + name);  
           }  
         } else {  
           log.debug("No external role found for the user: " + userName);  
         }  
       }  
       Collections.addAll(roles, names);  
       return (String[])roles.toArray(new String[roles.size()]);  
     }  
   }  

And with this you will need to modify your $CARBON_HOME/repository/conf/user-mgt.xml user store manager configuration section as below.

     <UserStoreManager class="org.wso2.carbon.userstore.jdbc.CustomJDBCUserStoreManager">  
       <Property name="TenantManager">org.wso2.carbon.user.core.tenant.JDBCTenantManager</Property>  
       <Property name="ReadOnly">false</Property>  
       <Property name="ReadGroups">true</Property>  
       <Property name="WriteGroups">true</Property>  
       <Property name="UsernameJavaRegEx">^[\S]{3,30}$</Property>  
       <Property name="UsernameJavaScriptRegEx">[a-zA-Z0-9@._-|//]{3,30}$</Property>  
       <Property name="UsernameWithEmailJavaScriptRegEx">[a-zA-Z0-9@._-|//]{3,30}$</Property>  
       <Property name="UsernameJavaRegExViolationErrorMsg">Username pattern policy violated</Property>  
       <Property name="PasswordJavaRegEx">^[\S]{5,30}$</Property>  
       <Property name="PasswordJavaScriptRegEx">^[\S]{5,30}$</Property>  
       <Property name="PasswordJavaRegExViolationErrorMsg">Password length should be within 5 to 30 characters</Property>  
       <Property name="RolenameJavaRegEx">^[\S]{3,255}$</Property>  
       <Property name="RolenameJavaScriptRegEx">^[\S]{3,255}$</Property>  
       <Property name="CaseInsensitiveUsername">true</Property>  
       <Property name="SCIMEnabled">false</Property>  
       <Property name="IsBulkImportSupported">false</Property>  
       <Property name="PasswordDigest">SHA-256</Property>  
       <Property name="StoreSaltedPassword">true</Property>  
       <Property name="MultiAttributeSeparator">,</Property>  
       <Property name="MaxUserNameListLength">100</Property>  
       <Property name="MaxRoleNameListLength">100</Property>  
       <Property name="UserRolesCacheEnabled">true</Property>  
       <Property name="UserNameUniqueAcrossTenants">false</Property>  
       <Property name="PasswordHashMethod">SHA</Property>  
       <Property name="SelectUserSQL">SELECT distinct u.* FROM UM_USER u left join UM_USER_ATTRIBUTE ua on u.UM_ID = ua.UM_USER_ID WHERE u.UM_USER_NAME = ? OR (ua.UM_ATTR_NAME = "mail" AND ua.UM_ATTR_VALUE = ?) AND u.UM_TENANT_ID = ?</Property>  
       <Property name="UserRoleSQLCaseInsensitive">SELECT UM_ROLE_NAME FROM UM_USER_ROLE, UM_ROLE, UM_USER WHERE LOWER(UM_USER.UM_USER_NAME) IN (SELECT LCASE(u.UM_USER_NAME) FROM UM_USER u left join UM_USER_ATTRIBUTE ua on u.UM_ID = ua.UM_USER_ID WHERE u.UM_USER_NAME = ? OR (ua.UM_ATTR_NAME = "mail" AND ua.UM_ATTR_VALUE = ?) AND u.UM_TENANT_ID = ? GROUP BY u.UM_USER_NAME) AND UM_USER.UM_ID=UM_USER_ROLE.UM_USER_ID AND UM_ROLE.UM_ID=UM_USER_ROLE.UM_ROLE_ID AND UM_USER_ROLE.UM_TENANT_ID = ? AND UM_ROLE.UM_TENANT_ID = ? AND UM_USER.UM_TENANT_ID = ?</Property>  
     </UserStoreManager>  


To use User Info endpoint with Oauth

Further extending this If you need to use User Info endpoint with oauth2, then you will need to furthe extend following method as well.

   public boolean doCheckExistingUser(String userName) throws UserStoreException {  
     String sqlStmt;  
     if(this.isCaseSensitiveUsername()) {  
       sqlStmt = this.realmConfig.getUserStoreProperty("IsUserExistingSQL");  
     } else {  
       sqlStmt = this.realmConfig.getUserStoreProperty("IsUserExistingSQLCaseInsensitive");  
     }  
     if(sqlStmt == null) {  
       throw new UserStoreException("The sql statement for is user existing null");  
     } else {  
       boolean isExisting = false;  
       String isUnique = this.realmConfig.getUserStoreProperty("UserNameUniqueAcrossTenants");  
       if(Boolean.parseBoolean(isUnique) && !"wso2.anonymous.user".equals(userName)) {  
         String uniquenesSql;  
         if(this.isCaseSensitiveUsername()) {  
           uniquenesSql = this.realmConfig.getUserStoreProperty("UserNameUniqueAcrossTenantsSQL");  
         } else {  
           uniquenesSql = this.realmConfig.getUserStoreProperty("UserNameUniqueAcrossTenantsSQLCaseInsensitive");  
         }  
         isExisting = this.isValueExisting(uniquenesSql, (Connection)null, new Object[]{userName});  
         if(log.isDebugEnabled()) {  
           log.debug("The username should be unique across tenants.");  
         }  
       } else if(sqlStmt.contains("UM_TENANT_ID")) {  
         isExisting = this.isValueExisting(sqlStmt, (Connection)null, new Object[]{userName, userName, Integer.valueOf(this.tenantId)});  
       } else {  
         isExisting = this.isValueExisting(sqlStmt, (Connection)null, new Object[]{userName});  
       }  
       return isExisting;  
     }  
   }  

And you will need to add more configurations and following is the updated user store manager configuration.

     <UserStoreManager class="org.wso2.sample.userstore.jdbc.CustomJDBCUserStoreManager">  
       <Property name="TenantManager">org.wso2.carbon.user.core.tenant.JDBCTenantManager</Property>  
       <Property name="ReadOnly">false</Property>  
       <Property name="ReadGroups">true</Property>  
       <Property name="WriteGroups">true</Property>  
       <Property name="UsernameJavaRegEx">^[\S]{3,30}$</Property>  
       <Property name="UsernameJavaScriptRegEx">[a-zA-Z0-9@._-|//]{3,30}$</Property>  
       <Property name="UsernameWithEmailJavaScriptRegEx">[a-zA-Z0-9@._-|//]{3,30}$</Property>  
       <Property name="UsernameJavaRegExViolationErrorMsg">Username pattern policy violated</Property>  
       <Property name="PasswordJavaRegEx">^[\S]{5,30}$</Property>  
       <Property name="PasswordJavaScriptRegEx">^[\S]{5,30}$</Property>  
       <Property name="PasswordJavaRegExViolationErrorMsg">Password length should be within 5 to 30 characters</Property>  
       <Property name="RolenameJavaRegEx">^[\S]{3,255}$</Property>  
       <Property name="RolenameJavaScriptRegEx">^[\S]{3,255}$</Property>  
       <Property name="CaseInsensitiveUsername">true</Property>  
       <Property name="SCIMEnabled">false</Property>  
       <Property name="IsBulkImportSupported">false</Property>  
       <Property name="PasswordDigest">SHA-256</Property>  
       <Property name="StoreSaltedPassword">true</Property>  
       <Property name="MultiAttributeSeparator">,</Property>  
       <Property name="MaxUserNameListLength">100</Property>  
       <Property name="MaxRoleNameListLength">100</Property>  
       <Property name="UserRolesCacheEnabled">true</Property>  
       <Property name="UserNameUniqueAcrossTenants">false</Property>  
       <Property name="PasswordHashMethod">SHA</Property>  
       <Property name="SelectUserSQL">SELECT distinct u.* FROM UM_USER u left join UM_USER_ATTRIBUTE ua on u.UM_ID = ua.UM_USER_ID WHERE u.UM_USER_NAME = ? OR (ua.UM_ATTR_NAME = "mail" AND ua.UM_ATTR_VALUE = ?) AND u.UM_TENANT_ID = ?</Property>  
       <Property name="UserRoleSQLCaseInsensitive">SELECT UM_ROLE_NAME FROM UM_USER_ROLE, UM_ROLE, UM_USER WHERE LOWER(UM_USER.UM_USER_NAME) IN (SELECT LCASE(u.UM_USER_NAME) FROM UM_USER u left join UM_USER_ATTRIBUTE ua on u.UM_ID = ua.UM_USER_ID WHERE u.UM_USER_NAME = ? OR (ua.UM_ATTR_NAME = "mail" AND ua.UM_ATTR_VALUE = ?) AND u.UM_TENANT_ID = ? GROUP BY u.UM_USER_NAME) AND UM_USER.UM_ID=UM_USER_ROLE.UM_USER_ID AND UM_ROLE.UM_ID=UM_USER_ROLE.UM_ROLE_ID AND UM_USER_ROLE.UM_TENANT_ID = ? AND UM_ROLE.UM_TENANT_ID = ? AND UM_USER.UM_TENANT_ID = ?</Property>  
       <Property name="GetUserPropertiesForProfileSQLCaseInsensitive">SELECT UM_ATTR_NAME, UM_ATTR_VALUE FROM UM_USER_ATTRIBUTE, UM_USER WHERE (UM_USER.UM_ID = UM_USER_ATTRIBUTE.UM_USER_ID OR (UM_USER_ATTRIBUTE.UM_ATTR_NAME = 'mail' AND LOWER(UM_USER_ATTRIBUTE.UM_ATTR_VALUE) = LOWER(?))) AND UM_PROFILE_ID=? AND UM_USER_ATTRIBUTE.UM_TENANT_ID=? AND UM_USER.UM_TENANT_ID=?</Property>  
       <Property name="IsUserExistingSQLCaseInsensitive">SELECT distinct u.UM_ID FROM UM_USER u left join UM_USER_ATTRIBUTE ua on u.UM_ID = ua.UM_USER_ID WHERE u.UM_USER_NAME = ? OR (ua.UM_ATTR_NAME = "mail" AND ua.UM_ATTR_VALUE = ?) AND u.UM_TENANT_ID = ?</Property>  
     </UserStoreManager>  

And further in $IS_HOME/repository/conf/identity/application-authentication.xml you will need to add following property under Authenticator Config for Basic Authenticator

 <Parameter name="UserNameAttributeClaimUri">http://wso2.org/claims/username</Parameter>  

So my Basic Authenticator Config tag is as below.

         <AuthenticatorConfig name="BasicAuthenticator" enabled="true">  
             <Parameter name="UserNameAttributeClaimUri">http://wso2.org/claims/username</Parameter>  
             <!--Parameter name="showAuthFailureReason">true</Parameter-->  
         </AuthenticatorConfig>  


With this you will be able to get configured claims when you logged in using different attributes.

References

[1] https://drive.google.com/file/d/0ByTCb2KmTk76dWMwcHMzbWJmVzA/view?usp=sharing

Comments

Popular posts from this blog

Generate JWT access tokens from WSO2 Identity Server

In Identity Server 5.2.0 we have created an interface to generate access tokens. Using that we have developed a sample to generate JWT tokens. You can find that sample under msf4j samples[1][2]. If you are build it as it is you will need to use Java 8 to build since msf4j is developed on Java 8. So you will need to run Identity Server on Java 8 as well. After building the project[2] please copy the jar inside target directory to $IS_HOME/repository/components/dropins/ directory. And then please add the following configuration to Identity.xml which is placed under $IS_HOME/repository/conf/identity/ folder inside tag OAuth . <IdentityOAuthTokenGenerator>com.wso2.jwt.token.builder.JWTAccessTokenBuilder</IdentityOAuthTokenGenerator> Then go to the database you used to store oauth tokens (This is the database pointed from the datasource you mentioned in the $IS_HOME/repository/conf/identity/identity.xml) and then alter the size of the column ACCESS_TOKEN of the tab

Oauth custom basic authenticator with WSO2 IS 5.1.0

WSO2 Identity Server supports Oauth2 authorization code grant type with basic authentication OOTB. But basic authentication is done only with WSO2 user store. So there could be use cases that basic authentication has to be done against some other system. In this case you follow below steps to achieve your requirement. First you need to create an class which extends AbstractApplicationAuthenticator and implements LocalApplicationAuthenticator. Because this class is going to act as your application authenticator so it needs to be an implementation of application authenticator interface and to achieve this it needs to be a local authenticator as well. [2] public class CustomBasicAuthenticator extends AbstractApplicationAuthenticator implements LocalApplicationAuthenticator {   Then you need to override the initiateAuthenticationRequest method so you can redirect to the page to enter user and password. In my sample I redirected to the page that is used by our default basic au

Integrate New Relic with WSO2 API Manager

In WSO2 API Manager, we have two transports. HTTP servlet transport and Passthru / NIO transport. All the web application requests are handled through HTTP servlet transport which is on 9763 port and 9443 port with ssl and here we are using tomcat inside WSO2 products. All the service requests are served via Passthru / NIO transport which is on 8082 and 8243 with ssl. When we integrate API Manager with new relic in the way discussed in blog posts [5],[6], new relic only detects the calls made to tomcat transports. So we couldn’t get the API calls related data OOTB. But by further analyzing new relic APIs I managed to find a workaround for this problem. New relic supports publishing custom events via their insights api[1]. So what we can do is publish these data via custom API handler[2]. Following is a sample implementation of a handler that I used to test the scenario. I will attach the full project herewith[7]. I have created an osgi bundle with this implementation so after building