Ian Boyd
Ian Boyd

Reputation: 257029

What is the native equivalent of PrincipalContext.ValidateCredentials?

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:

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:

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.

Upvotes: 11

Views: 2138

Answers (1)

Bondolin
Bondolin

Reputation: 3121

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.

Upvotes: 0

Related Questions