10

What is the native way to validate a set of user domain credentials (username, password, domain) against that domain?

In other words, i am looking for the native equivalent of:

Boolean ValidateCredentials(String username, String password, String domain)
{
   // create a "principal context" - e.g. your domain (could be machine, too)
   using(PrincipalContext pc = new PrincipalContext(ContextType.Domain, domain))
   {
      // validate the credentials
      return pc.ValidateCredentials(username, password)
   }
}

ValidateCredentials("iboyd", "Tr0ub4dor&3", "contoso");

Hasn't this been asked, and answered, to death?

No! This question is asked, a lot. Three of those times by me. But the more you dig into it, the more you realize the accepted answers are incorrect.

Microsoft managed to solve it in .NET with the PrincipalContext class added in .NET 3.5. And the PrincipalContext is nothing magical. Underneath it uses the flat C-style ldap API. But trying to reverse engineer the code from ILSpy is not working out. And referencesource notwithstanding, Microsoft still keeps large portions of the .NET Framework class library source code secret.

What have you tried?

Method 1: Just use LogonUser

I cannot use LogonUser. LogonUser works only when your machine is either on the domain you're validating (e.g. contoso) or your domain trusts the domain you're validating. In other words, if there is a domain controller out there for the contoso.test domain then:

LogonUser("iboyd", "contoso.test", "Tr0ub4dor&3", 
      LOGON32_LOGON_NETWORK, "Negotiate", ref token);

will fail with error:

1326 (Logon failure: unknown user name or bad password)

That's because this domain i specify is not my own domain, or a domain i trust.

C# PrincipalContext doesn't suffer from this problem.

Method 2: Just use the SSPI (Security Support Provider Interface)

The SSPI is what LogonUser uses internally. The short answer is that it fails for the same reason that LogonUser does: Windows will not trust credentials from an untrusted domain.

The code is pretty long to provide an example of. The psuedo-code jist is:

QuerySecurityPackageInfo("Negotiate");

// Prepare client message (negotiate)
AcquireCredentialsHandle(....); //for the client
InitializeSecurityContext(...); //on the returned client handle
CompleteAuthToken(...); //on the client context

// Prepare server message (challenge).    
AcquireCredentialsHandle(...); //for the server
AcceptSecurityContext(...); //on the returned server handle
CompleteAuthToken(...); //on the server context

// Prepare client message (authenticate).
AcquireCredentialsHandle(....); //for the client
InitializeSecurityContext(...); //on the returned client handle
CompleteAuthToken(...); //on the client context

// Prepare server message (authentication).
AcquireCredentialsHandle(...); //for the server
AcceptSecurityContext(...); //on the returned server handle
CompleteAuthToken(...); //on the server context

This code works great if your machine is joined to the domain you're validating credentials. But as soon as you try to validate a set of domain credentails from a foreign domain: it fails.

C# PrincipalContext doesn't suffer from this problem.

Method 3: Just use LDAP's AdsGetObject

Some might suggest using AdsGetObject.

AdsGetObject("LDAP://CN=iboyd,DC=contoso,DC=test");

That's a red-herring, because AdsGetObject supports no way to pass username/password:

HRESULT ADsGetObject(
  _In_   LPCWSTR lpszPathName,
  _In_   REFIID riid,
  _Out_  VOID **ppObject
);

Instead you will be simply asking about a user.

Perhaps you meant AdsOpenObject:

HRESULT ADsOpenObject(
  _In_   LPCWSTR lpszPathName,
  _In_   LPCWSTR lpszUserName,
  _In_   LPCWSTR lpszPassword,
  _In_   DWORD dwReserved,
  _In_   REFIID riid,
  _Out_  VOID **ppObject
);

where you can specify credentials to connect as.

C# PrincipalContext doesn't suffer from this problem.

Method 4: Just use AdsOpenObject

Some might suggest using AdsOpenObject:

String path = "LDAP://CN=iboyd,DC=contoso,DC=test"
AdsOpenObject(path, "iboyd", "Tr0ub4dor&3", 0, IADs, ref ads);

Setting aside the fact that the path i constructed is invalid, setting aside the fact that there is no way to construct a valid LDAP path when you only know:

  • {username}
  • {password}
  • {domain}

that is because

LDAP://CN={username},DC={domain}

is not a valid LDAP path for any user.

Notwithstanding the LDAP path conundrum, the fundamental issue is that trying to query LDAP. That is wrong.

We want to validate a user's AD credentials.

Any time we attempt to query an LDAP server, we will fail if the user does not have permission to query LDAP - even though their credentials are valid.

When you pass credentials to AdsOpenObject they are used to specify who you want to connect as. Once you connect, you will then perform a query against LDAP. When you don't have permission to query LDAP, the AdsOpenObject will fail.

What's even more maddening is that even if you do have permission to query for users in LDAP, you're still needlessly performing a query of LDAP - an expensive operation.

C# PrincipalContext doesn't suffer from this problem.

Method 5: Use ADO with the ADsDSOObject provider

There are many who simply use the ADsDSOObject OLEDB provider with ADO to query LDAP. This solves the issue of having to come up with the correct LDAP path - you don't have to know the LDAP path for the user

String sql = 
    'SELECT userAccountControl FROM "LDAP://DC=contoso,DC=test"
    'WHERE objectClass="user" 
    'and sAMAccountName = "iboyd"';

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

Connection conn = new ADODB.Connection();
conn.ConnectionString = connectionString;
conn.Open();
IRecordset rs = conn.Execute(sql);

That works; it solves the problem of not knowing a user's LDAP path. But it doesn't solve the issue of if you don't have permission to query AD, then it fails.

Plus there's the issue that it is querying Active Directory, when it should be validating credentials.

C# PrincipalContext doesn't suffer from this problem.

Method 6: Just use PrincipalContext.ValidateCredentials

The .NET 3.5 class PrincipalContext has that lets you validate credentials knowing only:

  • Username
  • Password
  • Domain Name

You don't need to know the name or IP of the AD server. You don't need to construct any LDAP paths. And most importantly, you don't need permission to query Active Directory - it just works.

I tried digging down into the source code using ILSpy, but it gets harry fast:

ValidateCredentials
   CredentialValidator.Validate
      BindLdap
         new LdapDirectoryIdentifier
         new LdapConnection
         ldapConnection.SessionOptions.FastConcurrentBind();
            lockedLdapBind
                Bind

With a lot of presumably important code around it. There's a lot of ups, and downs, with dependency injection, and functions too little - all the normal difficulties you get with overly complicated code structure. The complexity is on par with programming the SSPI. Nobody understands SSPI code, and i already wrote code that calls it!

Note: This question doesn't ask how to validate local credentials, as opposed to local credentials. Nor does it ask how to do both. In this case i'm simply asking how to do what is already available in the .NET world but in the native world.

Unfortunately:

System.Security.DirectoryServices.AccountManagement.PrincipalContext

was not exposed though a COM-callable wrapper:

enter image description here

And now that i've spent two and a half hours typing in this question: it is time to go home. Lets see if i get closed between now and tomorrow morning.

Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219
  • 1
    I wouldn't know the answer, sorry, but as an idea: would it be possible to create your own COM-callable wrapper? Perhaps as a last resort? – andlabs Mar 19 '15 at 03:00
  • Have you figured it out? – esskar Mar 12 '20 at 20:31
  • I would be glad to see if you have ever found an answer.. – managerger Apr 27 '22 at 09:04
  • 1
    Another issue with LogonUser() is if password is not valid, you get "An account failed to log on." event in Security event log, which is not desirable as you only validate credentials, and not logging on the user. SSPI does not have this problem, but cannot validate blank passwords. Not sure about PrincipalContext.ValidateCredentials, and do not really care as we can use only native code. – Oleg Korzhukov May 20 '23 at 16:13

1 Answers1

0

What about the LogonUser functions from advapi.dll, e.g. LogonUserA:

BOOL LogonUserA(
  [in]           LPCSTR  lpszUsername,
  [in, optional] LPCSTR  lpszDomain,
  [in, optional] LPCSTR  lpszPassword,
  [in]           DWORD   dwLogonType,
  [in]           DWORD   dwLogonProvider,
  [out]          PHANDLE phToken
);
LogonUser(L"LocalService", L"NT AUTHORITY", NULL, LOGON32_LOGON_SERVICE, LOGON32_PROVIDER_DEFAULT, &hToken)

This logs in against the AD on the local machine.

Bondolin
  • 2,793
  • 7
  • 34
  • 62