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.

4 ответа

Решение

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

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

Однако вы не сможете вызвать это в контексте пользователя, которого вы аутентифицируете; вам понадобится привилегированная учетная запись - я считаю, что это требование SE_TCB_NAME (также известный как 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);

Я знаю, что этот ответ опоздал на несколько лет, но мы столкнулись с той же ситуацией, что и оригинальный постер. К сожалению, в нашей среде мы не можем использовать 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.

Проверка "истечение срока действия пароля" относительно проста - по крайней мере, в 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, который надежно сообщает вам, заблокирована ли учетная запись или нет.

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

Марк

Вот атрибуты 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
Другие вопросы по тегам