18

It has been asked, and answered for .NET, but now it's time to get an answer for native Win32 code:

How do i validate a Windows username and password?

i asked this question before for managed code. Now it's time for the native solution.


It needs to be pointed the pitfalls with some of the more commonly proposed solutions:

Invalid Method 1. Query Active Directory with Impersonation

A lot of people suggest querying the Active Directory for something. If an exception is thrown, then you know the credentials are not valid - as is suggested in this stackoverflow question.

There are some serious drawbacks to this approach however:

  • You are not only authenticating a domain account, but you are also doing an implicit authorization check. That is, you are reading properties from the AD using an impersonation token. What if the otherwise valid account has no rights to read from the AD? By default all users have read access, but domain policies can be set to disable access permissions for restricted accounts (and or groups).

  • Binding against the AD has a serious overhead, the AD schema cache has to be loaded at the client (ADSI cache in the ADSI provider used by DirectoryServices). This is both network, and AD server, resource consuming - and is too expensive for a simple operation like authenticating a user account.

  • You're relying on an exception failure for a non-exceptional case, and assuming that means invalid username and password. Other problems (e.g. network failure, AD connectivity failure, memory allocation error, etc) are then mis-intrepreted as authentication failure.

The use of the DirectoryEntry class is .NET is an example of an incorrect way to verify credentials:

Invalid Method 1a - .NET

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

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

As well as querying Active Directory through an ADO connection:

Invalid Method 1c - Native Query

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'

These both fail even when your credentials are valid, but you do not have permission to view your directory entry:

enter image description here

Invalid Method 2. LogonUser Win32 API

Others have suggested using the LogonUser() API function. This sounds nice, but unfortunatly the calling user sometimes needs a permission ususally only given to the operating system itself:

The process calling LogonUser requires the SE_TCB_NAME privilege. If the calling process does not have this privilege, LogonUser fails and GetLastError returns ERROR_PRIVILEGE_NOT_HELD.

In some cases, the process that calls LogonUser must also have the SE_CHANGE_NOTIFY_NAME privilege enabled; otherwise, LogonUser fails and GetLastError returns ERROR_ACCESS_DENIED. This privilege is not required for the local system account or accounts that are members of the administrators group. By default, SE_CHANGE_NOTIFY_NAME is enabled for all users, but some administrators may disable it for everyone.

Handing out the "Act as a part of the operating system" privelage is not something you want to do willy-nilly - as Microsoft points out in a knowledge base article:

...the process that is calling LogonUser must have the SE_TCB_NAME privilege (in User Manager, this is the "Act as part of the Operating System" right). The SE_TCB_NAME privilege is very powerful and should not be granted to any arbitrary user just so that they can run an application that needs to validate credentials.

Additionally, a call to LogonUser() will fail if a blank password is specified.


Valid .NET 3.5 Method - PrincipalContext

There is a validation method, only available in .NET 3.5 and newer, that allows authentication by a user without performing an authorization check:

// 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")
}

Unfortunately this code is only available in .NET 3.5 and later.

It's time to find the native equivalent.

Community
  • 1
  • 1
Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219
  • Just curious, why don't you try checking it with dotPeek? – Andrey Agibalov Aug 18 '11 at 17:38
  • From the description of PrincipalContext.ValidateCredentials(..) I read that it uses bind to the LDAP with the credentials specified in the PrincipalContext constructor (or in case of your sample code using the default principal) and then validates the specified credentials specified in the ValidateCredentials call. Therefore you need an fixed account which has the permission to bind to the LDAP/AD. – Robert Aug 18 '11 at 17:43
  • @loki2302 i tried digging into `PrincipalContext` in Reflector, but it got real messy real fast. – Ian Boyd Aug 18 '11 at 17:44
  • If you really need something that runs also on NT 4.0 and Windows 2000, http://support.microsoft.com/kb/180548 which you link to includes sample code for using SSPI to validate scredentials. – John Aug 18 '11 at 21:43
  • PrincipalContext::ValidateCredentials() calls into CredentialValidate::BindLdap() for directory credentials, so any native solution that uses LDAP would be just as valid. – Luke Aug 18 '11 at 21:43

5 Answers5

9

Here is Microsoft's recommendation.

As for the other answers, I'm not really sure why you're shooting them down. You are complaining about (relatively edge case) failures while trying to validate credentials, but if you are going to actually do something with those credentials then that operation is just going to fail anyway. If you are not going to actually do something with those credentials, then why do you need to validate them in the first place? It seems like a somewhat contrived situation, but obviously I don't know what you're trying to accomplish.

Luke
  • 11,211
  • 2
  • 27
  • 38
  • +1 for just try it. I've never liked this approach, but it continues to be the [simplest approach.](http://blogs.msdn.com/b/oldnewthing/archive/2004/06/04/148426.aspx) Oh, and I'd +1 again if I could for that KB link. – J.J. Aug 18 '11 at 21:42
  • That was actually what I was thinking of when writing this answer; I love his blog. – Luke Aug 18 '11 at 21:45
  • I just need to know if the entered username/password is valid. I won't be trying to impersonate them, open a process, or in any other way use the credentials. I just need to know that they're valid. And yes, some corporate clients still use windows 2000. – Ian Boyd Aug 19 '11 at 03:09
  • 1
    Problem with "just try it" is that it leads to false negatives. I figure rather than randomly trying things, and hope I find one that looks like it should work on all versions of windows, I can ask knowledgeable people - who know the *intended* mechanism to validate a username/password (considering windows has been validating passwords for 20 years now) – Ian Boyd Aug 19 '11 at 03:15
  • A careful reading of `LogonUser`, the supplied link, and the SSPI documentation hints that `LogonUser` uses SSPI (as the linked KB article recommends). `LogonUser` then performs additional work in order to create a user token (or impersonation token, depending on parameters). So i presume that the linked code is guaranteed to work all circumstances, on all versions of Windows, in all cases, for all types of callers, for now and all future versions of Windows. – Ian Boyd Aug 19 '11 at 13:17
  • LogonUser also fails (for a major government client) in cross-domain situations. – Ian Boyd Jul 10 '15 at 14:47
4

For the native equivalnt of your valid .NET solution see this MSDN page and ldap_bind

Howerver I think that LogonUser is the right API for the task when use with LOGON32_LOGON_NETWORK. Note that the limitation of SE_CHANGE_NOTIFY_NAME is only for Windows 2000 (so Windows XP and newer do not require this priviledge) and that by default SE_CHANGE_NOTIFY_NAME is enabled for all users. Also the MSDN page says

The SE_TCB_NAME privilege is not required for this function unless you are logging onto a Passport account.

In this case you are logging onto an AD account so SE_TCB_NAME is not required.

John
  • 5,561
  • 1
  • 23
  • 39
  • Active Directory is not guaranteed to allow LDAP connections. – Andrey Agibalov Aug 18 '11 at 18:38
  • 1
    This is the native equivalent of the only "valid" example given in the question. I would suggest `LogonUsser` with `LOGON32_LOGON_NETWORK` but for some reason Ian thinks this API is not acceptable. – John Aug 18 '11 at 18:47
  • 1
    @John It's not that i don't consider it acceptable, it's that sometimes it won't work. – Ian Boyd Aug 18 '11 at 20:59
  • The limitations that you list are only apply to Windows 2000 and older. For Windows XP and newer you do not need SE_TCB_NAME nor SE_CHANGE_NOTIFY_NAME . – John Aug 18 '11 at 21:41
  • @John i do have corporations using Windows 2000. i had started using GDI+, and had to scramble because Windows 2000 didn't ship with GDI+ (it was standard starting with Windows XP). At least with GDI+, i could mail them `gdiplus.dll`. With `LogonUser` i would frantically have to find *another* solution. Fortunately there is one: using SSPI directly, as the accepted linked KB article says. – Ian Boyd Aug 19 '11 at 13:20
2

I might as well post the native code to validate a set of Windows credentials. It took a while to implement.

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;

Note: Any code released into public domain. No attribution required.

Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219
  • I am struggling with similar things. In my case, I need a method that will work reliably also under heavy load. I am afraid that SSPI fails in this area too. I have not done any isolated testing of this authentication. But I am judging that based on experience. E.g. a simple call like: NetUserGetInfo() fails randomly under heavy stress. So I have no hope that something more complicated like SSPI could work reliably... – Martin Dobšík Jul 23 '14 at 11:32
  • I'm trying to use your code on Delphi 10 Seattle but the part: `authIdentity.Domain:= PChar(Domain);` Gives an error: Incompatible types PUSHORT and PWideChar... All the _SEC_WINNT_AUTH_IDENTITY_ attributions give this problem (Domain, Username, Password) – user2864778 Jun 21 '17 at 13:33
1

There is a win32 API function called ldap_bind_s. The ldap_bind_s function authenticates a client against LDAP. See MSDN documentation for more information.

Hans
  • 12,902
  • 2
  • 57
  • 60
-3

I authenticated user, by username & password like this :

username is user sn attribute value in Ldap server, like U12345

userDN is user DistinguishedName in 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;
}

}

nahidf
  • 2,260
  • 1
  • 15
  • 22
  • 2
    That fails if the user does not have permission to query active directory. Their credentials are valid; they're just not allowed to look at the Active Directory server. Also the serious overhead i mention in **Invalid Method #1** in the question. And finally, it relies on exceptions for non-exceptional circumstances. – Ian Boyd Apr 04 '14 at 18:30