Reputation: 257029
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");
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.
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.
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.
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.
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.
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.
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:
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
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