Win32: Как проверить учетные данные в Active Directory? - программирование
Подтвердить что ты не робот

Win32: Как проверить учетные данные в Active Directory?

Он был спросил, а ответил для .NET, но теперь пришло время получить ответ для собственного кода Win32:

Как проверить имя пользователя и пароль Windows?

i задал этот вопрос раньше для управляемого кода. Теперь пришло время для собственного решения.


Нужно указать на подводные камни с некоторыми из наиболее распространенных решений:

Недействительный метод 1. Запрос Active Directory с олицетворением

Многие люди предлагают запрашивать Active Directory. Если выбрано исключение, то вы знаете, что учетные данные недействительны - как это предлагается в этом вопросе о стеке_потока.

Есть некоторые серьезные недостатки этого подхода:

  • Вы не только аутентифицируете учетную запись домена, но также выполняете неявную проверку авторизации. То есть вы читаете свойства из AD, используя маркер олицетворения. Что делать, если в противном случае действительная учетная запись не имеет права читать из AD? По умолчанию все пользователи имеют доступ на чтение, но для политик домена можно отключить разрешения доступа для ограниченных учетных записей (или групп).

  • Привязка к AD имеет серьезные накладные расходы, кеш AD-схемы должен быть загружен на клиенте (кеш ADSI в поставщике ADSI, используемом DirectoryServices). Это как сетевой, так и серверы AD, потребляющие ресурсы - и слишком дороги для простой операции, такой как аутентификация учетной записи пользователя.

  • Вы полагаетесь на отказ исключения для случая, отличного от исключительных, и считаете, что это означает неправильное имя пользователя и пароль. Другие проблемы (например, сбой в сети, отказ подключения AD, ошибка выделения памяти и т.д.) Затем неправильно интерпретируются как сбой аутентификации.

Использование класса DirectoryEntry - это пример неправильного способа проверки учетных данных:

Недействительный метод 1a -.NET

DirectoryEntry entry = new DirectoryEntry("persuis", "iboyd", "Tr0ub4dor&3");
object nativeObject = entry.NativeObject;

Недействительный метод 1b -.NET # 2

public static Boolean CheckADUserCredentials(String accountName, String password, String domain)
{
    Boolean result;

    using (DirectoryEntry entry = new DirectoryEntry("LDAP://" + domain, accountName, password))
    {
        using (DirectorySearcher searcher = new DirectorySearcher(entry))
        {
            String filter = String.Format("(&(objectCategory=user)(sAMAccountName={0}))", accountName);
            searcher.Filter = filter;
            try
            {
                SearchResult adsSearchResult = searcher.FindOne();
                result = true;
            }
            catch (DirectoryServicesCOMException ex)
            {
                const int SEC_E_LOGON_DENIED = -2146893044; //0x8009030C;
                if (ex.ExtendedError == SEC_E_LOGON_DENIED)
                {
                    // Failed to authenticate. 
                    result = false;
                }
                else
                {
                    throw;
                }
            }
        }
    }

Как и запрос Active Directory через соединение ADO:

Недействительный метод 1c - Исходный запрос

connectionString = "Provider=ADsDSOObject;
       User ID=iboyd;Password=Tr0ub4dor&3;
       Encrypt Password=True;Mode=Read;
       Bind Flags=0;ADSI Flag=-2147483648';"

SELECT userAccountControl 
FROM 'LDAP://persuis/DC=stackoverflow,DC=com'
WHERE objectClass='user' and sAMAccountName = 'iboyd'

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

enter image description here

Недействительный метод 2. LogonUser Win32 API

Другие предложили использовать LogonUser() API. Это звучит неплохо, но, к сожалению, вызывающему пользователю иногда требуется разрешение, доступное только для самой операционной системы:

Для вызова процесса LogonUser требуется привилегия SE_TCB_NAME. Если вызывающий процесс не имеет этого привилегии, LogonUser выходит из строя и GetLastError возвращает ERROR_PRIVILEGE_NOT_HELD.

В некоторых случаях, процесс, который вызывает LogonUser также должен иметь SE_CHANGE_NOTIFY_NAME привилегия включен; в противном случае LogonUser не работает и GetLastError возвращается ERROR_ACCESS_DENIED. Эта привилегия не требуется для локальной системы учетные записи или учетные записи, которые являются членами группы администраторов. От по умолчанию, SE_CHANGE_NOTIFY_NAME для всех пользователей, но некоторые администраторы могут отключить его для каждый.

Отправка "Акта как части операционной системы" - это не то, что вы хотите делать волей-неволей - как указывает Microsoft в статья базы знаний:

... процесс, вызывающий LogonUser должен иметь SE_TCB_NAME привилегии (в User Manager это "Закон как часть действующего Система" справа). SE_TCB_NAME привилегия очень сильна и не должны предоставляться произвольным пользователям, чтобы они могли запустите приложение, которое должно проверять учетные данные.

Кроме того, вызов LogonUser() завершится с ошибкой, если указан пустой пароль.


Действительный метод .NET 3.5 - PrincipalContext

Существует метод проверки, доступный только в .NET 3.5 и новее, который позволяет проверять подлинность пользователя без проверки авторизации:

// create a "principal context" - e.g. your domain (could be machine, too)
using(PrincipalContext pc = new PrincipalContext(ContextType.Domain, "stackoverflow.com"))
{
    // validate the credentials
    bool isValid = pc.ValidateCredentials("iboyd", "Tr0ub4dor&3")
}

К сожалению, этот код доступен только в .NET 3.5 и более поздних версиях.

Пришло время найти родной эквивалент.

4b9b3361

Ответ 1

Вот рекомендация Microsoft.

Что касается других ответов, я не совсем уверен, почему вы их снимаете. Вы жалуетесь на неудачи (относительно крайнего случая) при попытке проверить учетные данные, но если вы собираетесь что-то делать с этими учетными данными, тогда эта операция просто сработает. Если вы не собираетесь делать что-то с этими учетными данными, то зачем вам сначала их проверять? Это похоже на несколько надуманную ситуацию, но, очевидно, я не знаю, чего вы пытаетесь выполнить.

Ответ 2

Для собственного эквивалента вашего действительного .NET-решения см. this страница MSDN и ldap_bind

Howerver Я считаю, что LogonUser является правильным API для задачи при использовании с LOGON32_LOGON_NETWORK. Обратите внимание, что ограничение SE_CHANGE_NOTIFY_NAME предназначено только для Windows 2000 (так что Windows XP и новее не требуют этой привилегии), и что по умолчанию SE_CHANGE_NOTIFY_NAME разрешен для всех пользователей. Также на странице MSDN

Для этой функции привилегия SE_TCB_NAME не требуется, если вы не входите в учетную запись Passport.

В этом случае вы регистрируетесь на учетной записи AD, поэтому SE_TCB_NAME не требуется.

Ответ 3

Я мог бы также опубликовать собственный код для проверки набора учетных данных Windows. Это потребовалось некоторое время для реализации.

function TSSPLogon.LogonUser(username, password, domain: string; packageName: string='Negotiate'): HRESULT;
var
    ss: SECURITY_STATUS;
    packageInfo: PSecPkgInfoA;
    cbMaxToken: DWORD;
    clientBuf: PByte;
    serverBuf: PByte;
    authIdentity: SEC_WINNT_AUTH_IDENTITY;
    cbOut, cbIn: DWORD;
    asClient: AUTH_SEQ;
    asServer: AUTH_SEQ;
    Done: boolean;
begin
{
    If domain is blank will use the current domain.
    To force validation against the local database use domain "."

    sspiProviderName is the same of the Security Support Provider Package to use. Some possible choices are:
            - Negotiate (Preferred)
                        Introduced in Windows 2000 (secur32.dll)
                        Selects Kerberos and if not available, NTLM protocol.
                        Negotiate SSP provides single sign-on capability called as Integrated Windows Authentication.
                        On Windows 7 and later, NEGOExts is introduced which negotiates the use of installed
                        custom SSPs which are supported on the client and server for authentication.
            - Kerberos
                        Introduced in Windows 2000 and updated in Windows Vista to support AES) (secur32.dll)
                        Preferred for mutual client-server domain authentication in Windows 2000 and later.
            - NTLM
                        Introduced in Windows NT 3.51 (Msv1_0.dll)
                        Provides NTLM challenge/response authentication for client-server domains prior to
                        Windows 2000 and for non-domain authentication (SMB/CIFS)
            - Digest
                        Introduced in Windows XP (wdigest.dll)
                        Provides challenge/response based HTTP and SASL authentication between Windows and non-Windows systems where Kerberos is not available
            - CredSSP
                        Introduced in Windows Vista and available on Windows XP SP3 (credssp.dll)
                        Provides SSO and Network Level Authentication for Remote Desktop Services
            - Schannel
                        Introduced in Windows 2000 and updated in Windows Vista to support stronger AES encryption and ECC (schannel.dll)
                        Microsoft implementation of TLS/SSL
                        Public key cryptography SSP that provides encryption and secure communication for
                        authenticating clients and servers over the internet. Updated in Windows 7 to support TLS 1.2.

    If returns false, you can call GetLastError to get the reason for the failure
}


    // Get the maximum authentication token size for this package
    ss := sspi.QuerySecurityPackageInfoA(PAnsiChar(packageName), packageInfo);
    if ss <> SEC_E_OK then
    begin
        RaiseWin32Error('QuerySecurityPackageInfo "'+PackageName+'" failed', ss);
        Result := ss;
        Exit;
    end;

    try
        cbMaxToken := packageInfo.cbMaxToken;
    finally
        FreeContextBuffer(packageInfo);
    end;

    // Initialize authorization identity structure
    ZeroMemory(@authIdentity, SizeOf(authIdentity));
    if Length(domain) > 0 then
    begin
        authIdentity.Domain := PChar(Domain);
        authIdentity.DomainLength := Length(domain);
    end;

    if Length(userName) > 0 then
    begin
        authIdentity.User := PChar(UserName);
        authIdentity.UserLength := Length(UserName);
    end;

    if Length(Password) > 0 then
    begin
        authIdentity.Password := PChar(Password);
        authIdentity.PasswordLength := Length(Password);
    end;

    AuthIdentity.Flags := SEC_WINNT_AUTH_IDENTITY_ANSI; //SEC_WINNT_AUTH_IDENTITY_UNICODE

    ZeroMemory(@asClient, SizeOf(asClient));
    ZeroMemory(@asServer, SizeOf(asServer));

    //Allocate buffers for client and server messages
    GetMem(clientBuf, cbMaxToken);
    GetMem(serverBuf, cbMaxToken);
    try
        done := False;
        try
            // Prepare client message (negotiate)
            cbOut := cbMaxToken;
            ss := Self.GenClientContext(@asClient, authIdentity, packageName, nil, 0, clientBuf, cbOut, done);
            if ss < 0 then
            begin
                RaiseWin32Error('Error generating client context for negotiate', ss);
                Result := ss;
                Exit;
            end;

            // Prepare server message (challenge).
            cbIn := cbOut;
            cbOut := cbMaxToken;
            ss := Self.GenServerContext(@asServer, packageName, clientBuf, cbIn, serverBuf, cbOut, done);
            if ss < 0 then
            begin
                {
                    Most likely failure: AcceptServerContext fails with SEC_E_LOGON_DENIED in the case of bad username or password.
                    Unexpected Result:   Logon will succeed if you pass in a bad username and the guest account is enabled in the specified domain.
                }
                RaiseWin32Error('Error generating server message for challenge', ss);
                Result := ss;
                Exit;
            end;

            // Prepare client message (authenticate).
            cbIn := cbOut;
            cbOut := cbMaxToken;
            ss := Self.GenClientContext(@asClient, authIdentity, packageName, serverBuf, cbIn, clientBuf, cbOut, done);
            if ss < 0 then
            begin
                RaiseWin32Error('Error generating client client for authenticate', ss);
                Result := ss;
                Exit;
            end;

            // Prepare server message (authentication).
            cbIn := cbOut;
            cbOut := cbMaxToken;
            ss := Self.GenServerContext(@asServer, packageName, clientBuf, cbIn, serverBuf, cbOut, done);
            if ss < 0 then
            begin
                RaiseWin32Error('Error generating server message for authentication', ss);
                Result := ss;
                Exit;
            end;
        finally
            //Free resources in client message
            if asClient.fHaveCtxtHandle then
                sspi.DeleteSecurityContext(@asClient.hctxt);

            if asClient.fHaveCredHandle then
                sspi.FreeCredentialHandle(@asClient.hcred);

            //Free resources in server message
            if asServer.fHaveCtxtHandle then
                sspi.DeleteSecurityContext(@asServer.hctxt);

            if asServer.fHaveCredHandle then
                sspi.FreeCredentialHandle(@asServer.hcred);
        end;
    finally
        FreeMem(clientBuf);
        FreeMem(serverBuf);
    end;

    Result := S_OK;
end;

Примечание. Любой код, выпущенный в общественное достояние. Не требуется атрибуция.

Ответ 4

Существует функция win32 API, называемая ldap_bind_s. Функция ldap_bind_s выполняет аутентификацию клиента против LDAP. Дополнительную информацию см. В документации MSDN.

Ответ 5

Я аутентифицировал пользователя, используя имя пользователя и пароль:

имя пользователя - это значение атрибута пользователя sn на сервере Ldap, например U12345

userDN - это пользователь DistinguishedName в LdapServer

public bool AuthenticateUser(string username, string password)
{
try
{
var ldapServerNameAndPort = "Servername:389";
var userDN = string.Format("CN=0},OU=Users,OU=MyOU,DC=MyDC,DC=com",username);
var conn = new LdapConnection(ldapServerNameAndPort)
{
 AuthType = AuthType.Basic
};
conn.Bind(new NetworkCredential(userDN , password));
return true;
}
catch (Exception e)
{
 return false;
}

}