Active Directory (LDAP) - проверка заблокированной учетной записи/срок действия пароля истек

В настоящее время я аутентифицирую пользователей от некоторых AD, используя следующий код:

DirectoryEntry entry = new DirectoryEntry(_path, username, pwd);

try
{
    // Bind to the native AdsObject to force authentication.
    Object obj = entry.NativeObject;

    DirectorySearcher search = new DirectorySearcher(entry) { Filter = "(sAMAccountName=" + username + ")" };
    search.PropertiesToLoad.Add("cn");
    SearchResult result = search.FindOne();
    if (result == null)
    {
        return false;
    }
    // Update the new path to the user in the directory
    _path = result.Path;
    _filterAttribute = (String)result.Properties["cn"][0];
}
catch (Exception ex)
{
    throw new Exception("Error authenticating user. " + ex.Message);
}

Это отлично работает для проверки пароля с именем пользователя.

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

Однако аутентификация может также завершиться неудачей, если учетная запись заблокирована.

Как я узнаю, если он не работает из-за его блокировки?

Я столкнулся с статьями, которые вы можете использовать:

Convert.ToBoolean(entry.InvokeGet("IsAccountLocked"))

или выполните что-то вроде объяснения здесь

Проблема заключается в том, что всякий раз, когда вы пытаетесь получить доступ к любому свойству в DirectoryEntry, выдается такая же ошибка.

Любое другое предположение о том, как добраться до фактической причины неудачи аутентификации? (заблокирована учетная запись/пароль/и т.д.)

AD, к которому я подключаюсь, не обязательно должен быть сервером Windows.

Ответы

Ответ 1

Немного поздно, но я выброшу это.

Если вы хотите ДЕЙСТВИТЕЛЬНО определить конкретную причину отказа аутентификации (существует еще много причин, кроме неправильного пароля, истек, блокировка и т.д.), вы можете использовать API-интерфейс Windows API LogonUser. Не пугайтесь этого - это проще, чем кажется. Вы просто вызываете LogonUser, и если он не работает, вы смотрите на Marshal.GetLastWin32Error(), который даст вам код возврата, который указывает (очень) конкретную причину, по которой не удалось выполнить вход.

Однако вы не сможете назвать это в контексте пользователя, который вы аутентифицируете; вам понадобится привилегированная учетная запись - я полагаю, что это SE_TCB_NAME (aka SeTcbPrivilege) - учетная запись пользователя, которая имеет право "действовать как часть операционной системы".

//Your new authenticate code snippet:
        try
        {
            if (!LogonUser(user, domain, pass, LogonTypes.Network, LogonProviders.Default, out token))
            {
                errorCode = Marshal.GetLastWin32Error();
                success = false;
            }
        }
        catch (Exception)
        {
            throw;
        }
        finally
        {
            CloseHandle(token);    
        }            
        success = true;

Если это не удается, вы получаете один из кодов возврата (есть больше, что вы можете посмотреть вверх, но это важные из них:

 //See http://support.microsoft.com/kb/155012
    const int ERROR_PASSWORD_MUST_CHANGE = 1907;
    const int ERROR_LOGON_FAILURE = 1326;
    const int ERROR_ACCOUNT_RESTRICTION = 1327;
    const int ERROR_ACCOUNT_DISABLED = 1331;
    const int ERROR_INVALID_LOGON_HOURS = 1328;
    const int ERROR_NO_LOGON_SERVERS = 1311;
    const int ERROR_INVALID_WORKSTATION = 1329;
    const int ERROR_ACCOUNT_LOCKED_OUT = 1909;      //It gives this error if the account is locked, REGARDLESS OF WHETHER VALID CREDENTIALS WERE PROVIDED!!!
    const int ERROR_ACCOUNT_EXPIRED = 1793;
    const int ERROR_PASSWORD_EXPIRED = 1330;  

Остальное - это просто копировать/вставлять, чтобы получить DLLImports и значения для передачи в

  //here are enums
    enum LogonTypes : uint
        {
            Interactive = 2,
            Network =3,
            Batch = 4,
            Service = 5,
            Unlock = 7,
            NetworkCleartext = 8,
            NewCredentials = 9
        }
        enum LogonProviders : uint
        {
            Default = 0, // default for platform (use this!)
            WinNT35,     // sends smoke signals to authority
            WinNT40,     // uses NTLM
            WinNT50      // negotiates Kerb or NTLM
        }

//Paste these DLLImports

[DllImport("advapi32.dll", SetLastError = true)]
        static extern bool LogonUser(
         string principal,
         string authority,
         string password,
         LogonTypes logonType,
         LogonProviders logonProvider,
         out IntPtr token);

[DllImport("kernel32.dll", SetLastError = true)]
        static extern bool CloseHandle(IntPtr handle);

Ответ 2

Я знаю, что этот ответ на несколько лет опоздал, но мы просто столкнулись с той же ситуацией, что и оригинальный плакат. К сожалению, в нашей среде мы не можем использовать LogonUser - нам нужно было чистое решение LDAP. Оказывается, есть способ получить расширенный код ошибки из операции привязки. Это немного уродливо, но оно работает:

catch(DirectoryServicesCOMException exc)
{
    if((uint)exc.ExtendedError == 0x80090308)
    {
        LDAPErrors errCode = 0;

        try
        {
            // Unfortunately, the only place to get the LDAP bind error code is in the "data" field of the 
            // extended error message, which is in this format:
            // 80090308: LdapErr: DSID-0C09030B, comment: AcceptSecurityContext error, data 52e, v893
            if(!string.IsNullOrEmpty(exc.ExtendedErrorMessage))
            {
                Match match = Regex.Match(exc.ExtendedErrorMessage, @" data (?<errCode>[0-9A-Fa-f]+),");
                if(match.Success)
                {
                    string errCodeHex = match.Groups["errCode"].Value;
                    errCode = (LDAPErrors)Convert.ToInt32(errCodeHex, fromBase: 16);
                }
            }
        }
        catch { }

        switch(errCode)
        {
            case LDAPErrors.ERROR_PASSWORD_EXPIRED:
            case LDAPErrors.ERROR_PASSWORD_MUST_CHANGE:
                throw new Exception("Your password has expired and must be changed.");

            // Add any other special error handling here (account disabled, locked out, etc...).
        }
    }

    // If the extended error handling doesn't work out, just throw the original exception.
    throw;
}

И вам понадобятся определения кодов ошибок (их гораздо больше в http://www.lifeasbob.com/code/errorcodes.aspx):

private enum LDAPErrors
{
    ERROR_INVALID_PASSWORD = 0x56,
    ERROR_PASSWORD_RESTRICTION = 0x52D,
    ERROR_LOGON_FAILURE = 0x52e,
    ERROR_ACCOUNT_RESTRICTION = 0x52f,
    ERROR_INVALID_LOGON_HOURS = 0x530,
    ERROR_INVALID_WORKSTATION = 0x531,
    ERROR_PASSWORD_EXPIRED = 0x532,
    ERROR_ACCOUNT_DISABLED = 0x533,
    ERROR_ACCOUNT_EXPIRED = 0x701,
    ERROR_PASSWORD_MUST_CHANGE = 0x773,
    ERROR_ACCOUNT_LOCKED_OUT = 0x775,
    ERROR_ENTRY_EXISTS = 0x2071,
}

Я не мог найти эту информацию нигде - все говорят, что вы должны использовать LogonUser. Если будет лучшее решение, я бы с удовольствием это услышал. Если нет, я надеюсь, что это поможет другим людям, которые не могут вызвать LogonUser.

Ответ 3

Проверка "истекает срок действия пароля" относительно проста - по крайней мере, в Windows (не знаете, как это делают другие системы): когда значение Int64 для "pwdLastSet" равно 0, пользователь должен будет изменить его (или ее) пароль при следующем входе в систему. Самый простой способ проверить это - включить это свойство в свой DirectorySearcher:

DirectorySearcher search = new DirectorySearcher(entry)
      { Filter = "(sAMAccountName=" + username + ")" };
search.PropertiesToLoad.Add("cn");
search.PropertiesToLoad.Add("pwdLastSet");

SearchResult result = search.FindOne();
if (result == null)
{
    return false;
}

Int64 pwdLastSetValue = (Int64)result.Properties["pwdLastSet"][0];

Что касается проверки "учетной записи заблокирован" - сначала это кажется легким, но это не так.... Флаг "UF_Lockout" в "userAccountControl" не выполняет свою работу надежно.

Начиная с Windows 2003 AD, есть новый вычисленный атрибут, который вы можете проверить: msDS-User-Account-Control-Computed.

Учитывая DirectoryEntry user, вы можете сделать:

string attribName = "msDS-User-Account-Control-Computed";
user.RefreshCache(new string[] { attribName });

const int UF_LOCKOUT = 0x0010;

int userFlags = (int)user.Properties[attribName].Value;

if(userFlags & UF_LOCKOUT == UF_LOCKOUT) 
{
   // if this is the case, the account is locked out
}

Если вы можете использовать .NET 3.5, все стало намного проще - ознакомьтесь с статьей MSDN о том, как бороться с пользователями и групп в .NET 3.5 с использованием пространства имен System.DirectoryServices.AccountManagement. Например. теперь у вас есть свойство IsAccountLockedOut в классе UserPrincipal, которое надежно сообщает вам, заблокирована ли учетная запись или нет.

Надеюсь, это поможет!

Марк

Ответ 4

Вот атрибуты AD LDAP, которые изменяются для пользователя, когда пароль заблокирован (первое значение), в отличие от того, когда пароль не заблокирован (второе значение). badPwdCount и lockoutTime, очевидно, наиболее актуальны. Я не уверен, что uSNChanged и whenChanged должны обновляться вручную или нет.

$ diff LockedOut.ldif NotLockedOut.ldif:

< badPwdCount: 3
> badPwdCount: 0

< lockoutTime: 129144318210315776
> lockoutTime: 0

< uSNChanged: 8064871
> uSNChanged: 8065084

< whenChanged: 20100330141028.0Z
> whenChanged: 20100330141932.0Z