Strange issue with System.DirectoryServices.AccountManagement.UserPrincipal.FindByIdentity
Asked Answered
I

1

13

We're writing a system that allows a user to change their account password through a web application on our intranet.

At first, everything appeared to be running smoothly. During development passwords for our test accounts could be changed with no problem.

When we made the system live, however, we started running into issues. Here are the symptoms:

  1. At first, everything is fine. Users can change their passwords.
  2. At some point, the following error occurs in UserPrincipal.FindByIdentity: "System.Runtime.InteropServices.COMException: The authentication mechanism is unknown. "
  3. From then on, trying to change a password through the web application results in the error: "System.Runtime.InteropServices.COMException: The server is not operational. "
  4. If I manually recycle the app pool, everything seems to fix itself until more errors begin happening... i.e., the process starts all over again at phase 1.

Here's the relevant snippet of code:


    private static PrincipalContext CreateManagementContext() {
        return new PrincipalContext(
            ContextType.Domain, 
            ActiveDirectoryDomain, 
            ActiveDirectoryManagementAccountName,
            ActiveDirectoryManagementAccountPassword);
    }


    private static void ChangeActiveDirectoryPasword(string username, string password) {
        if (username == null) throw new ArgumentNullException("username");
        if (password == null) throw new ArgumentNullException("password");

        using (var context = CreateManagementContext())
        using (var user = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, username)) {
            user.SetPassword(password);
        }
    }

Any clues as to why this is happening? Google searches aren't turning up anything really helpful, and neither are the docs on MSDN.

Insole answered 10/12, 2009 at 18:50 Comment(2)
What do you do have for ActiveDirectoryManagementAccountName - try setting this to "domain\username" instead of just "username" - this has worked for me in the past.Heartstricken
I'll try that, though I doubt it's the issue. I would be more likely to suspect an problem of that nature if the errors weren't so intermittent. The code works, and works well, until something seemingly random triggers a spiraling wormhole of death. I would suspect to get consistent errors if there were a problem with the account name. However, this is System.DirectoryServices we're talking about... so any kind of funky bad strangeness is possible ;)Insole
S
11

First thing I notice is that you are using UserPrincipal.FindByIdentity which is inherited from AuthenticablePrincipal which is inherited from Principal. I say all this because the Principal class has a known memory leak in the FindByIdentity. If you have a look at this MSDN entry, you will notice at the bottom that Gary Caldwell from Microsoft said the following:

This call has an unmanaged memory leak because the underlying implemenation uses DirectorySearcher and SearchResultsCollection but does not call dispose on the SearchResultsCollection as the document describes.

I would guess that this is your issue. The memory leak causes the Application Pool to fill up and finally cause errors, until the Application Pool is reset and the memory is disposed.

When we use any active directory functions, we use the following to accomplish setting of the user's password:

Public Shared Function GetUserAccount(ByVal username As String) As DirectoryEntry
    Dim rootPath As String = GetRootPath()
    Using objRootEntry As New DirectoryEntry(rootPath)
        Using objAdSearcher As New DirectorySearcher(objRootEntry)
            objAdSearcher.Filter = "(&(objectClass=user)(samAccountName=" & username & "))"
            Dim objResult As SearchResult = objAdSearcher.FindOne()
            If objResult IsNot Nothing Then Return objResult.GetDirectoryEntry()
        End Using
    End Using
    Return Nothing
End Function

Public Shared Sub SetPassword(ByVal username As String, ByVal newPassword As String)
    Using objUser As DirectoryEntry = GetUserAccount(username)
        If objUser Is Nothing Then Throw New UserNotFoundException(username)
        Try
            objUser.Invoke("SetPassword", newPassword)
            objUser.CommitChanges()
        Catch ex As Exception
            Throw New Exception("Could not change password for " & username & ".", ex)
        End Try
    End Using
End Sub

Also, if you're wanting the users to change the passwords directly and you don't want to rely on their honesty, you might want to consider using the ChangePassword function of LDAP like this:

Public Shared Sub ChangePassword(ByVal username As String, ByVal oldPassword As String, ByVal newPassword As String)
    Using objUser As DirectoryEntry = GetUserAccount(username)
        If objUser Is Nothing Then Throw New UserNotFoundException(username)
        Try
            objUser.Invoke("ChangePassword", oldPassword, newPassword)
            objUser.CommitChanges()
        Catch ex As TargetInvocationException
            Throw New Exception("Could not change password for " & username & ".", ex)
        End Try
    End Using
End Sub

This forces the user to know the prior password before changing to the new one.

I hope this helps,

Thanks!

Swirl answered 10/12, 2009 at 23:28 Comment(5)
Definitely helps. We've installed a hotfix which supposedly addresses some (but not all) of the issues with FindByIdentity, and are testing our code now. While we would prefer to use the ChangePassword method, we unfortunately have some funkiness in our domain that's enforcing password rules that we don't want. SetPassword bypasses those rules. If our current attempts to fix the existing code don't work, the next step will be to rewrite it using System.DirectoryServices directly (no AccountManagement stuff), as you demonstrated.Insole
Nick, when I implement your above mentioned method of doing this, it seems that it also checks the domain user account password policies. I would assume this is how it would work because under the hood, it uses the same methods. Also, looking at this MSDN article: msdn.microsoft.com/en-us/library/… you see that SetPassword can generate a PasswordException. It also states that it throws this if the new password does not meet password complexity requirements. Hope this helps!Swirl
Under the hood, ChangePassword is calling DirectoryEntry.Invoke("ChangePassword", password). SetPassword is calling DirectoryEntry.Invoke("SetPassword", password). Our policies are being enforced when using ChangePassword (things like not allowing past passwords to be reused). They are not enforced with SetPassword. With SetPassword, I can set anyone's account password to whatever I please if I have the correct permissions. If you look here: forums.asp.net/p/1488874/3493645.aspx you will notice that someone else has also discovered this. The MSDN docs are misleading on this.Insole
Nick - is this the hot fix you are referring to? code.msdn.microsoft.com/KB969166/Release/… - JeffWellordered
I'm having a similar issue that I have posted. If any of you have a found a workaround. Please comment on this post. #10291509Torritorricelli

© 2022 - 2024 — McMap. All rights reserved.