Win32:如何验证针对Active Directory的凭据?

时间:2011-08-18 17:33:21

标签: windows security winapi authentication active-directory

它已经askedanswered for .NET,但现在是时候得到原生Win32代码的答案了:

如何验证Windows用户名和密码?

asked this question before for managed code。现在是原生解决方案的时候了。


需要指出一些更常见的解决方案的陷阱:

无效方法1.使用模拟查询Active Directory

很多人suggest querying the Active Directory为了某事。如果抛出异常,则表示凭据无效 - 如this stackoverflow question中所述。

然而有some serious drawbacks to this approach

  
      
  • 您不仅要对域帐户进行身份验证,还要进行隐式授权检查。也就是说,您正在使用模拟令牌从AD中读取属性。如果其他有效帐户无权从AD读取,该怎么办?默认情况下,所有用户都具有读取权限,但可以将域策略设置为禁用受限制帐户(和/或组)的访问权限。

  •   
  • 绑定AD会产生严重的开销,必须在客户端加载AD架构缓存(DirectoryServices使用的ADSI提供程序中的ADSI缓存)。这是网络和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;
                }
            }
        }
    }

以及通过ADO连接查询Active Directory:

无效的方法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

Others建议使用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是   为所有用户启用,但有些用户启用   管理员可以禁用它   每个人。

将“法案作为操作系统的一部分”发布出来并不是你想要做的事情 - 正如微软在knowledge base article中指出的那样:

  

......正在呼叫的过程   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 个答案:

答案 0 :(得分:9)

Here is Microsoft's recommendation

至于其他答案,我不确定你为什么要击落它们。您在尝试验证凭据时抱怨(相对边缘的情况)失败,但如果您要实际使用这些凭据执行某些操作,那么该操作无论如何都会失败。如果您不打算使用这些凭据实际执行某些操作,那么为什么您需要首先验证它们?这似乎是一个有点人为的情况,但显然我不知道你想要完成什么。

答案 1 :(得分:4)

对于有效.NET解决方案的本机等效内容,请参阅this MSDN页面和ldap_bind

Howerver我认为LogonUser是与LOGON32_LOGON_NETWORK一起使用时该任务的正确API。请注意,SE_CHANGE_NOTIFY_NAME的限制仅适用于Windows 2000(因此Windows XP及更高版本不需要此特权),并且默认情况下为所有用户启用SE_CHANGE_NOTIFY_NAME。 MSDN页面也说

  
    

除非您登录Passport帐户,否则此功能不需要SE_TCB_NAME权限。

  

在这种情况下,您登录的是AD帐户,因此不需要SE_TCB_NAME。

答案 2 :(得分:2)

我不妨发布本机代码来验证一组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;
  

注意:任何已发布到公共领域的代码。无需归属。

答案 3 :(得分:1)

有一个名为ldap_bind_s的win32 API函数。 ldap_bind_s函数验证客户端 针对LDAP。有关详细信息,请参阅MSDN文档。

答案 4 :(得分:-3)

我通过用户名&amp ;;验证了用户身份密码是这样的:

username是Ldap服务器中的用户sn属性值,如U12345

userDN是LdapServer中的用户DistinguishedName

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;
}

}