Почему Active Directory проверяет последний пароль?

Я работаю над простым решением для обновления пароля пользователя в Active Directory.

Я могу успешно обновить пароль пользователей. Обновление пароля работает отлично. Допустим, пользователь обновил пароль от MyPass1 до MyPass2

Теперь, когда я запускаю свой собственный код для проверки учетных данных пользователей, используя:

using(PrincipalContext pc = new PrincipalContext(ContextType.Domain, "TheDomain"))
{
    // validate the credentials
    bool isValid = pc.ValidateCredentials("myuser", "MyPass2");
}

//returns true - which is good

Теперь, когда я ввожу неверный пароль, он очень хорошо проверяется:

using(PrincipalContext pc = new PrincipalContext(ContextType.Domain, "TheDomain"))
{
    // validate the credentials
    bool isValid = pc.ValidateCredentials("myuser", "wrongPass");
}

//returns false - which is good

Теперь по каким-то нечетным причинам он проверяет предыдущий пароль, который помнил MyPass1?

using(PrincipalContext pc = new PrincipalContext(ContextType.Domain, "TheDomain"))
{
    // validate the credentials
    bool isValid = pc.ValidateCredentials("myuser", "MyPass1");
}

//returns true - but why? we have updated password to Mypass2

Я получил этот код:

Подтвердить имя пользователя и пароль в Active Directory?

Является ли это чем-то связанным с последним истечением срока действия пароля или это то, как должна работать валидация?

Ответ 1

Причина, по которой вы видите это, связана с особым поведением, характерным для сетевой проверки подлинности NTLM.

Вызов метода ValidateCredentials в экземпляре PrincipalContext приводит к созданию защищенного соединения LDAP, за которым следует операция привязки к этому соединению с помощью вызова функции ldap_bind_s.

Метод аутентификации, используемый при вызове ValidateCredentials, составляет AuthType.Negotiate. Используя этот результат в попытке связывания с использованием Kerberos, который (будучи, конечно, не NTLM), не проявляет особого поведения, описанного выше. Однако попытка привязки с использованием Kerberos завершится неудачно (неверный пароль и все), что приведет к другой попытке сделать это, используя NTLM.

У вас есть два способа приблизиться к этому:

  • Следуйте инструкциям в статье Microsoft KB, которую я связал, чтобы сократить или исключить срок службы старого пароля, используя значение реестра OldPasswordAllowedPeriod. Вероятно, это не самое идеальное решение.
  • Не используйте класс PrincipleContext для проверки учетных данных. Теперь, когда вы знаете (примерно), как работает ValidateCredentials, вам не должно быть слишком сложно выполнить этот процесс вручную. То, что вам нужно сделать, это создать новое соединение LDAP (LdapConnection), установить его учетные данные сети, явно указать AuthType на AuthType.Kerberos, а затем вызвать Bind(). Вы получите исключение, если учетные данные плохие.

В следующем коде показано, как можно выполнить проверку достоверности с использованием только Kerberos. Метод аутентификации при использовании не будет возвращаться к NTLM в случае сбоя.

private const int ERROR_LOGON_FAILURE = 0x31;

private bool ValidateCredentials(string username, string password, string domain)
{
  NetworkCredential credentials
    = new NetworkCredential(username, password, domain);

  LdapDirectoryIdentifier id = new LdapDirectoryIdentifier(domain);

  using (LdapConnection connection = new LdapConnection(id, credentials, AuthType.Kerberos))
  {
    connection.SessionOptions.Sealing = true;
    connection.SessionOptions.Signing = true;

    try
    {
      connection.Bind();
    }
    catch (LdapException lEx)
    {
      if (ERROR_LOGON_FAILURE == lEx.ErrorCode)
      {
        return false;
      }
      throw;
    }
  }
  return true;
}

Я пытаюсь никогда не использовать исключения для управления потоком моего кода; однако в этом конкретном случае единственным способом проверки учетных данных в LDAP-соединении, по-видимому, является попытка операции Bind, которая генерирует исключение, если учетные данные являются плохими. PrincipalContext использует тот же подход.

Ответ 3

Я нашел способ проверить только текущие учетные данные пользователя. Он использует тот факт, что ChangePassword не использует кэшированные учетные данные. Пытаясь изменить пароль на его текущее значение, которое сначала проверяет пароль, мы можем определить, является ли пароль неправильным или есть проблема политики (не может повторно использовать один и тот же пароль дважды).

Примечание: это, вероятно, будет работать только в том случае, если ваша политика имеет требование истории, по крайней мере, не позволяющее повторять самый последний пароль.

        var isPasswordValid = PrincipalContext.ValidateCredentials(
            userName,
            password);

        // use ChangePassword to test credentials as it doesn't use caching, unlike ValidateCredentials
        if (isPasswordValid)
        {
            try
            {
                user.ChangePassword(password, password);
            }
            catch (PasswordException ex)
            {
                if (ex.InnerException != null && ex.InnerException.HResult == -2147024810)
                {
                    // Password is wrong - must be using a cached password
                    isPasswordValid = false;
                }
                else
                {
                    // Password policy problem - this is expected, as we can't change a password to itself for history reasons    
                }
            }
            catch (Exception)
            {
                // ignored, we only want to check wrong password. Other AD related exceptions should occure in ValidateCredentials
            }
        }