A couple of months ago, my colleague Robert Shurtleff (@rgsiii) came to me with an interesting idea about a CRM add on to change AD passwords. After a few emails back and forth thrashing out the design, it was time to make it actually work. In this post, I am going to provide some of the details of a solution to enable a user to change his or her AD password from within CRM.
Business Case:
User needs to reset the AD directory password from CRM 2011. The solution should work from a machine which is not a member of the target AD forest or domain.
Design:
We will add a new button called "Password Reset" to the user form. Access to the form can be controlled based on security role privilege. Clicking on the reset button will open a new form for a custom entity. The custom entity will capture the password details, and if the password reset fails, the error message is inserted into a multi line text field. There is also a field that captures when the password change attempt was made.
Implementation Details:
1. Create a new button on the User form. I highly recommend using the Ribbon Workbench tool from Develop1. Examples are available on their site that will walk you through the process.
2. Create a new custom entity called "Password Details". Add the following fields:
- Domain\User (text)
- Old Password (text)
- New Password (text)
- Retype New Password (text)
- Password Change Attempt (text)
- Error details (multi-line text)
3. Add javascript that will be called by the button created on the user form. The js will open a new "Password Details" record, with the domain\user value carried over from the User form.
function OpenNewUserPwd() { var dValue = Xrm.Page.getAttribute("domainname").getValue(); var parameters = {}; parameters["new_name"] = dValue; // Open the window. Xrm.Utility.openEntityForm("new_userpassworddetails", null, parameters); }
4. The values for the old password, new password and confirm new password are set as required fields in the UI level only, as we don't want to have a record of these values in the database. For my sample case, I am showing the password as clear text in the form right now. The error details multi line text field and the domain\user text field are both read only.
5. On save of the "Password Details" entity record, a pre-create plugin fires that does the password change. Note that the reason to use a pre-create plugin is to clear the password details in plugin code before saving the record. The only data we want to see after the save is the domain\user, password change attempt time and error text, if any.
Here is the code snippet to get you started:
namespace AD_Password_Plugin { public class ResetPwd : IPlugin { [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] static extern bool LogonUser(string principal, string authority, string password, LogonSessionType logonType, LogonProvider logonProvider, out IntPtr token); [DllImport("kernel32.dll", CharSet = CharSet.Auto)] public extern static bool CloseHandle(IntPtr handle); #region << public Enum >> enum LogonSessionType : uint { Interactive = 2, Network, Batch, Service, NetworkCleartext = 8, NewCredentials } enum LogonProvider : uint { Default = 0, // default for platform WinNT35, // sends smoke signals to authority WinNT40, // uses NTLM WinNT50 // negotiates Kerb or NTLM } #endregion public void Execute(IServiceProvider serviceProvider) { ITracingService tracingService = (ITracingService)serviceProvider.GetService(typeof(ITracingService)); // Obtain the execution context from the service provider. Microsoft.Xrm.Sdk.IPluginExecutionContext context = (Microsoft.Xrm.Sdk.IPluginExecutionContext) serviceProvider.GetService(typeof(Microsoft.Xrm.Sdk.IPluginExecutionContext)); // The InputParameters collection contains all the data passed in the message request. if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity) { // Obtain the target entity from the input parmameters. Entity entity = (Entity)context.InputParameters["Target"]; string dName= ""; // Verify that the target entity represents a User Password Details entity. // If not, this plug-in was not registered correctly. if (entity.LogicalName == "new_userpassworddetails") { // Check if password exists try { IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory)); IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId); if (entity.Attributes.Contains("new_newpassword")) { string uPassword = entity.GetAttributeValue<String>("new_newpassword"); string oldPassword = entity.GetAttributeValue<string>("new_oldpassword"); string confirmPassword = entity.GetAttributeValue<string>("new_confirmnewpassword"); dName = entity.GetAttributeValue<string>("new_name"); //check if new password matches the confirm new password value if (uPassword == confirmPassword) { string domainName = ""; string uName = ""; String[] userPath = dName.Split(new char[] { '\\' }); if (userPath.Length > 1) { domainName = userPath[0]; uName = userPath[1]; } else { domainName = String.Empty; uName = string.Empty; } #region ############### impersonate ################################# //impersonate IntPtr token = IntPtr.Zero; WindowsImpersonationContext impersonatedUser = null; string aAccount = uName; //string userDn = ADExtensionMethods.GetObjectDistinguishedName(uName, domainName); string userDn = GetObjectDistinguishedName(uName, domainName); tracingService.Trace("User Dn value {0}", userDn); try { //impersonate function tracingService.Trace("Inside impersonation section"); bool result = LogonUser(aAccount, domainName, oldPassword, LogonSessionType.Network, LogonProvider.Default, out token); if (result) { WindowsIdentity id = new WindowsIdentity(token); impersonatedUser = id.Impersonate(); string user_name = WindowsIdentity.GetCurrent().Name.ToString(); tracingService.Trace(" impersonation user {0}", user_name); //change the user password try { tracingService.Trace("Trying to change the user password"); DirectoryEntry uEntry = new DirectoryEntry(userDn, user_name, oldPassword); tracingService.Trace("change the user password"); uEntry.Invoke("ChangePassword", new object[] { oldPassword, uPassword }); tracingService.Trace("commit change to the user password"); uEntry.CommitChanges(); } catch (Exception e) { //entity.Attributes.Add("new_errormessage", "Password Change Failed! \r\n"+e.ToString()); if (entity.Attributes.Contains("new_errormessage") == true) { entity.Attributes["new_errormessage"] = "Password Change Failed 1! \r\n" + e.ToString(); } else { entity.Attributes.Add("new_errormessage", "Password Change Failed 2! \r\n" + e.ToString()); } tracingService.Trace("change pwd error {0}", e.ToString()); //throw e; } } //set error messag if the user cannot be reached with the password provided else { tracingService.Trace("Logon user cannot be reached"); string errorMessage = new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()).Message; string err_msg = "The logon user cannot be reached. Windows System Error Code: " + Marshal.GetLastWin32Error().ToString(); err_msg += " Error Description: " + errorMessage; //entity.Attributes.Add("new_errormessage", err_msg ); if (entity.Attributes.Contains("new_errormessage") == true) { entity.Attributes["new_errormessage"] = "Password Change Failed \r\n" + err_msg; } else { entity.Attributes.Add("new_errormessage", "Password Change Failed \r\n" + err_msg); } } } catch (Exception ex) { //set error message if password change failed if (entity.Attributes.Contains("new_errormessage") == true) { entity.Attributes["new_errormessage"] = "Password Change Failed! \r\n" + ex.ToString(); } else { entity.Attributes.Add("new_errormessage", "Password Change Failed! \r\n" + ex.ToString()); } tracingService.Trace("change pwd connect error {0}", ex.ToString()); //comment out later! //throw ex; } finally { // Stop impersonation and revert to the process identity if (impersonatedUser != null) impersonatedUser.Undo(); // Free the token if (token != IntPtr.Zero) CloseHandle(token); } //end impersonate #endregion } else { entity.Attributes.Add("new_errormessage", "The new password and confirm password values do not match"); } //set old and new passwords to empty strings before saving the record entity.Attributes["new_oldpassword"] = string.Empty; entity.Attributes["new_newpassword"]= string.Empty; entity.Attributes["new_confirmnewpassword"]= string.Empty; //set the PasswordChangedon datetime if (entity.Attributes.Contains("new_passwordchangedon") == true) { entity.Attributes["new_passwordchangedon"] = DateTime.Now.ToString(); } else { entity.Attributes.Add("new_passwordchangedon", DateTime.Now.ToString()); } } } catch (Exception ex) { tracingService.Trace("AD Reset plugin: {0}", ex.ToString()); throw; } } } } #region private methods //get ldap path private string FriendlyDomainToLdapDomain(string friendlyDomainName) { string ldapPath = null; try { DirectoryContext objContext = new DirectoryContext( DirectoryContextType.Domain, friendlyDomainName); Domain objDomain = Domain.GetDomain(objContext); ldapPath = objDomain.Name; } catch (DirectoryServicesCOMException e) { ldapPath = e.Message.ToString(); } return ldapPath; } //get distinguished name for user private string GetObjectDistinguishedName(string objectName, string LdapDomain) { string distinguishedName = string.Empty; string connectionPrefix = "LDAP://" + LdapDomain; DirectoryEntry entry = new DirectoryEntry(connectionPrefix); DirectorySearcher mySearcher = new DirectorySearcher(entry); mySearcher.Filter = "(&(objectClass=user)(|(cn=" + objectName + ")(sAMAccountName=" + objectName + ")))"; SearchResult result = mySearcher.FindOne(); if (result == null) { throw new NullReferenceException ("unable to locate the distinguishedName for the object " + objectName + " in the " + LdapDomain + " domain"); } DirectoryEntry directoryObject = result.GetDirectoryEntry(); distinguishedName = "LDAP://" + directoryObject.Properties ["distinguishedName"].Value; entry.Close(); entry.Dispose(); mySearcher.Dispose(); return distinguishedName; } #endregion } }
Let me walk you though the code a bit. If the new password and the confirm password match, first set the impersonation to change the password as the user whose password is changed. Using the user id and domain, search AD for the user and get the DistinguishedName. Once we have that, I invoke ChangePassword to set the AD password to the new value. I also clear the values of the all the password fields and set the Password Change Attempt on field.
Note that when there is an error, I continue saving the Password Detail form, and insert the error in to the multi line text field. This, along with the Password Change Attempt field and user field, gives me an audit trail of the folks who changed the password, the date/time of the password change attempt, and the errors if any during the process.
See below screenshot of the list of password attempts
See below an example of the error when trying to change the password.
Thanks for reading!
I got an error while executing change request , everything seems right there, It saying "Invalid Argument " on the line uEntry.Invoke("ChangePassword", new object[] { oldPassword, uPassword });
ReplyDeletecan you suggest what should i do ??
I reset password successfuly once for a user but now it giving an exception like ... Help me out on it
ReplyDeleteSystem.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.DirectoryServices.DirectoryServicesCOMException: A constraint violation occurred. (Exception from HRESULT: 0x8007202F)
--- End of inner exception stack trace ---
at System.DirectoryServices.DirectoryEntry.Invoke(String methodName, Object[] args)
at PasswordChangePlugin.Pwdchange.Execute(IServiceProvider serviceProvider)
Manoj,
DeleteThere are 2 parts to the password change feature - calling it from Dynamics CRM and then actually changing the password using AD services. The error you have seems to be AD related, and I would suggest trying to debug/ troubleshoot that section.
When i did a search for your error message, here is one possible reason:
http://www.powergui.org/thread.jspa?threadID=16593
Hope this helps!