I have an MVC 4 application that allows a user to change their Active Directory password through a password reset functionality page. I have the following piece of code that sets the new password:
DirectoryEntry de = sr.GetDirectoryEntry();
de.Invoke("SetPassword", new object[] { newPassword });
de.Properties["LockOutTime"].Value = 0;
Upon trying to submit the form with the new password details I am having the following error written to the application event log:
0x80070005 (E_ACCESSDENIED))
I have set the Identity property of the Application Pool to NetworkService and had thought that this would have resolved the issue of connecting. Is there anything else further I need to ensure so that my ASPNET application can connect to the AD.
tl;dr
In our case, this started happening randomly. Turns out it's because our self-signed SSL certificate had expired. After creating a new one in IIS, the problem was resolved.
Explanation
This thread lead me to the cause.
I will briefly recap what SetPassword
does here so you can see why you need it. That particular ADSI method really bundles 3 methods under the covers. It first tries to set the password over a secure SSL channel using LDAP. Next, it tries to set using Kerberos set password protocol. Finally, it uses NetUserSetInfo
to attempt to set it.
The main problem is that only the first two methods will generally respect the credentials that you put on the DirectoryEntry
. If you provide the proper credentials and an SSL channel for instance, the LDAP change password mechanism will work with those credentials...
If you check the NetUserSetInfo method, you will notice there is no place to put a username/password for authorization. In other words, it can only use the unmanaged thread's security context. This means that in order for it to work, it would have to impersonate the username/password combination you provided programmatically first...
LDAP over SSL is apparently the best way to go (and was the method that we had been using), and it appears (clarifications welcome) that once our self-signed SSL certificate had expired, it skipped Kerberos and fell back to NetUserSetInfo
, which failed because it was not using the credentials we provided. (Or it just failed on Kerberos, as the poster said he had never seen the credentials passed in for Kerberos)
So after creating a new self-signed certificate (for COMPUTER.DOMAIN.local
), the problem was resolved.
Here is the code (in case anyone's looking for it):
DirectoryEntry myDE = new DirectoryEntry(@"LDAP://OU=GroupName,DC=DOMAIN,DC=local");
myDE.Username = "administrator";
myDE.Password = "adminPassword";
DirectoryEntries myEntries = myDE.Children;
DirectoryEntry myDEUser = myEntries.Find("CN=UserName");
myDEUser.Invoke("SetPassword", new object[] { "NewPassword" });
myDEUser.Properties["LockOutTime"].Value = 0;
// the following 2 lines are free =)
myDEUser.Properties["userAccountControl"].Value = (int)myDEUser.Properties["userAccountControl"].Value | 0x10000; // don't expire password
myDEUser.Properties["userAccountControl"].Value = (int)myDEUser.Properties["userAccountControl"].Value & ~0x0002; // ensure account is enabled
myDEUser.CommitChanges();