Saturday, August 29, 2015

Change Password Validation Event Handler: Adding Custom Password Requirements

Tested On: Oracle Identity Manager 11.1.2.3.0
Description: Demonstrates how to add custom password requirements which are not covered by out of the box Oracle Identity Manager password policy. Implementation is handled by creating a custom validation event handler on change password operations. The example given here validates that the new password does not contain the user's middle name and email.

Validation on First Login Password Change

Validation on Forgot Password

Validation on Admin Changing User Password

References: https://docs.oracle.com/cd/E52734_01/oim/OMDEV/oper.htm#OMDEV3085
http://docs.oracle.com/cd/E52734_01/oim/OMUSG/pwdpolicy.htm#OMUSG5478
http://docs.oracle.com/cd/E27559_01/apirefs.1112/e28159/oracle/iam/platform/Platform.html#getServiceForEventHandlers_java_lang_Class__java_lang_String__java_lang_String__java_lang_String__java_util_HashMap_

package com.blogspot.oraclestack.eventhandlers;
import java.io.Serializable;
import java.util.HashMap;
import oracle.core.ojdl.logging.ODLLevel;
import oracle.core.ojdl.logging.ODLLogger;
import oracle.iam.platform.context.ContextAware;
import oracle.iam.platform.kernel.ValidationFailedException;
import oracle.iam.platform.kernel.spi.ValidationHandler;
import oracle.iam.platform.kernel.vo.BulkOrchestration;
import oracle.iam.platform.kernel.vo.Orchestration;
import com.thortech.xl.crypto.tcCryptoUtil;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashSet;
import javax.sql.DataSource;
import oracle.iam.identity.usermgmt.api.UserManager;
import oracle.iam.identity.usermgmt.api.UserManagerConstants;
import oracle.iam.identity.usermgmt.vo.User;
import oracle.iam.platform.Platform;
import oracle.iam.platform.context.ContextManager;
import oracle.iam.platform.kernel.ValidationException;
/**
* Additional password rules which are not handled by the OOTB Password Policy.
* Validate if the new password meets the custom password rules.
* @author rayedchan
*/
public class ChangePasswordValidationEH implements ValidationHandler
{
// Logger
private static final ODLLogger LOGGER = ODLLogger.getODLLogger(ChangePasswordValidationEH.class.getName());
// OIM API Services
// private static final UserManager USRMGR = Platform.getService(UserManager.class);
private static final UserManager USRMGR = Platform.getServiceForEventHandlers(UserManager.class, null, "ADMIN","ChangePasswordValidationEH", null);
// SQL Query
private static final String USER_ATTRS_SQL_QUERY = "SELECT usr_login, usr_middle_name, usr_email FROM usr where usr_key=?";
@Override
public void validate(long processId, long eventId, Orchestration orchestration)
{
LOGGER.log(ODLLevel.NOTIFICATION, "Version 1.0");
LOGGER.log(ODLLevel.NOTIFICATION, "Enter validate() with parameters: Process Id = [{0}], Event Id = [{1}], Orchestration = [{2}]", new Object[]{processId, eventId, orchestration});
Connection conn = null;
PreparedStatement ps = null;
User user = null;
try
{
// Get usr_key of target user
String usrKey = orchestration.getTarget().getEntityId();
LOGGER.log(ODLLevel.NOTIFICATION, "Target User USR_KEY: {0}", new Object[]{usrKey});
// Get actor
String actorLogin = ContextManager.getOIMUser();
LOGGER.log(ODLLevel.NOTIFICATION, "Actor Login: {0}", new Object[]{actorLogin});
// Contains only the new values
HashMap<String, Serializable> newParameters = orchestration.getParameters();
LOGGER.log(ODLLevel.TRACE, "Parameters: {0}", new Object[]{newParameters});
LOGGER.log(ODLLevel.TRACE, "InterEventData: {0}", new Object[]{orchestration.getInterEventData()}); // password policy info
LOGGER.log(ODLLevel.TRACE, "Context: {0}", new Object[]{orchestration.getContextVal()});
// Decrypt new password using the default secret key
String newPasswordEncrypted = getParamaterValue(newParameters, "usr_password");
String newPasswordDecrypted = tcCryptoUtil.decrypt(newPasswordEncrypted, "DBSecretKey");
LOGGER.log(ODLLevel.TRACE, "New Password: {0}", new Object[]{newPasswordDecrypted}); // TODO: Remove
// Anonymous user case E.g. Scenario Forget Password?
/*if("<anonymous>".equalsIgnoreCase(actorLogin))
{
// Get OIM database connection from data source
LOGGER.log(ODLLevel.NOTIFICATION, "Anonymous User");
DataSource ds = Platform.getOperationalDS(); // Get OIM datasource
conn = ds.getConnection(); // Get connection
LOGGER.log(ODLLevel.TRACE, "Got database connection.");
// Construct Prepared Statement
ps = conn.prepareStatement(USER_ATTRS_SQL_QUERY);
ps.setString(1, usrKey); // Set parametized value usr_key
// Execute query
ResultSet rs = ps.executeQuery();
// Iterate record; should be only one since usr_key is a primary key
while(rs.next())
{
// Get data from record
String middleName = rs.getString("usr_middle_name");
String email = rs.getString("usr_email");
String userLogin = rs.getString("usr_login");
// Construct user object
user = new User(usrKey);
user.setEmail(email);
user.setLogin(userLogin);
user.setMiddleName(middleName);
}
}
// All other cases (E.g. Administrator, Self)
else
{*/
// Get OIM User
HashSet<String> attrs = new HashSet<String>();
attrs.add(UserManagerConstants.AttributeName.MIDDLENAME.getId()); // Middle Name
attrs.add(UserManagerConstants.AttributeName.EMAIL.getId()); // Email
boolean useUserLogin = false;
user = USRMGR.getDetails(usrKey, attrs, useUserLogin);
//}
LOGGER.log(ODLLevel.NOTIFICATION, "User: {0}", new Object[]{user});
// Check password against custom rules
boolean validatePassword = this.customPasswordPolicy(newPasswordDecrypted, user);
LOGGER.log(ODLLevel.NOTIFICATION, "Is new password validate? {0}", new Object[]{validatePassword});
// Validation failed
if(!validatePassword)
{
throw new ValidationException("The following requirements have not been met. " + "(1) Must not contain middle name. (2) Must not contain email. ");
}
}
catch(Exception e)
{
LOGGER.log(ODLLevel.ERROR, "", e);
throw new ValidationFailedException(e);
}
finally
{
// Close statement
if(ps != null)
{
try
{
ps.close();
}
catch (SQLException ex)
{
LOGGER.log(ODLLevel.ERROR, "", ex);
}
}
// Close database connection
if(conn != null)
{
try
{
conn.close();
}
catch (SQLException ex)
{
LOGGER.log(ODLLevel.ERROR, "", ex);
}
}
}
}
@Override
public void validate(long processId, long eventId, BulkOrchestration bulkOrchestration)
{
LOGGER.log(ODLLevel.NOTIFICATION, "[NOT SUPPORTED] Enter validate() with parameters: Process Id = [{0}], Event Id = [{1}], Bulk Orchestration = [{2}]", new Object[]{processId, eventId, bulkOrchestration});
}
@Override
public void initialize(HashMap<String, String> hm)
{
LOGGER.log(ODLLevel.NOTIFICATION, "Enter initialize: {0}", new Object[]{hm});
}
/**
* ContextAware object is obtained when the actor is a regular user.
* If the actor is an administrator, the exact value of the attribute is obtained.
* @param parameters parameters from the orchestration object
* @param key name of User Attribute in OIM Profile or column in USR table
* @return value of the corresponding key in parameters
*/
private String getParamaterValue(HashMap<String, Serializable> parameters, String key)
{
String value = (parameters.get(key) instanceof ContextAware)
? (String) ((ContextAware) parameters.get(key)).getObjectValue()
: (String) parameters.get(key);
return value;
}
/**
* Custom Password Policy
* - Does not contain middle name
* - Does not contain email
* @param password Plain text password to validate
* @param user OIM User
* @return true if password requirements are met; false otherwise
*/
private boolean customPasswordPolicy(String password, User user)
{
// Fetch user's attributes
String middleName = user.getMiddleName(); // Get user's middle name
String email = user.getEmail(); // Get user's email
// Construct Regular Expression
String middleNameRegex = (middleName == null || "".equalsIgnoreCase(middleName)) ? "" : ".*(?i)" + middleName + ".*"; // contains, ignore case
String emailRegex = (email == null || "".equalsIgnoreCase(email)) ? "" : ".*(?i)" + email + ".*"; // contains, ignore case
// Check if password valid
boolean containsMiddleName = password.matches(middleNameRegex);
boolean containsEmail = password.matches(emailRegex);
boolean isValidatePassword = (!containsMiddleName) && (!containsEmail);
LOGGER.log(ODLLevel.TRACE, "Password contains middle name? {0}", new Object[]{containsMiddleName});
LOGGER.log(ODLLevel.TRACE, "Password contains email? {0}", new Object[]{containsEmail});
return isValidatePassword;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<eventhandlers xmlns="http://www.oracle.com/schema/oim/platform/kernel" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.oracle.com/schema/oim/platform/kernel orchestration-handlers.xsd">
<validation-handler
class="com.blogspot.oraclestack.eventhandlers.ChangePasswordValidationEH"
entity-type="User"
operation="CHANGE_PASSWORD"
name="ChangePasswordValidationEH"
order="1000"/>
</eventhandlers>
<oimplugins xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<plugins pluginpoint="oracle.iam.platform.kernel.spi.EventHandler">
<plugin name="ChangePasswordValidationEH" pluginclass="com.blogspot.oraclestack.eventhandlers.ChangePasswordValidationEH" version="1.0">
</plugin></plugins>
</oimplugins>
view raw plugin.xml hosted with ❤ by GitHub

Troubleshooting

Anonymous User Issue
Error: <oracle.iam.platform.authopss.impl> <BEA-000000> <Unable to populate the self-capabilities for User :null

Issue: When trying to change the user's password through the Forgot Password? link, the custom validation event handler fails when trying to use User Manager API.

Cause: Since the actor (the internal user performing the change) is anonymous, it fails when trying to call oracle.iam.identity.usermgmt.api.UserManager.getDetails method in the custom code when service is obtained by "Platform.getService(UserManager.class)". 

Workaround: The custom code has a check for <anonymous> user and performs a SQL query to get the target user's attributes or a much better approach is to use "Platform.getServiceForEventHandlers(UserManager.class, null, "ADMIN","ChangePasswordValidationEH", null)" to obtain the service. 

IAM-3040027 : An error occurred while changing the user password. java.lang.RuntimeException: Unable to populate the self-capabilities for User :null

Null Validation Message
Issue: When trying to change password through My Information section, the  validation message thrown in the custom validation event handler is not shown instead null is displayed. Looking at the logs this error may be the culprit:
<Error> <oracle.iam.platform.utils> <BEA-000000> <An error occurred while loading the parent resource bundle oracle.iam.selfservice.resources.Logging

3 comments:

  1. i get the following error when event handler is triggered..
    java.lang.NullPointerException
    at com.thortech.xl.crypto.tcDefaultDBEncryptionImpl.getCipher(tcDefaultDBEncryptionImpl.java:121)
    at com.thortech.xl.crypto.tcDefaultDBEncryptionImpl.decrypt(tcDefaultDBEncryptionImpl.java:215)
    at com.thortech.xl.crypto.tcCryptoUtil.decrypt(tcCryptoUtil.java:122)
    at com.thortech.xl.crypto.tcCryptoUtil.decrypt(tcCryptoUtil.java:163)
    at com.wa.wahbe.oim.eventhandlers.PasswordValidate.validate(Unknown Source)
    at oracle.iam.platform.kernel.impl.OrchProcessData.validate(OrchProcessData.java:258)
    Any help please ?

    ReplyDelete
  2. what is the solution for Null being displayed instead of error message when the password changed from my profile section?

    ReplyDelete
  3. The null message on the My Information page is a bug in OIM 12.2.1.3 also. I reported it. So hopefully it will get fixed.

    ReplyDelete