Win32: Как проверить учетные данные в Active Directory? - PullRequest
16 голосов
/ 18 августа 2011

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

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

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


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

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

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

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

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

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

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

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

Неверный метод 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

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

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

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

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

... процесс, который вызывает LogonUser должен иметь SE_TCB_NAME привилегия (в диспетчере пользователей это « Закон как часть операционной Система"справа). 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 и более поздних версиях.

Пора найти эквивалент native .

Ответы [ 5 ]

9 голосов
/ 19 августа 2011

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

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

4 голосов
/ 18 августа 2011

Эквивалент вашего действующего решения .NET см. На этой странице MSDN и ldap_bind

Однако я думаю, что 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 не требуется.

2 голосов
/ 04 апреля 2014

Я мог бы также опубликовать собственный код для проверки набора учетных данных 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's 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;

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

1 голос
/ 18 августа 2011

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

0 голосов
/ 04 апреля 2014

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

username - это значение атрибута user 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;
}

}

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...