title: What is the native equivalent of PrincipalContext.ValidateCredentials?
body: <p>What is the native way to validate a set of user domain credentials (username, password, domain) against that domain?</p>
  
  <p>In other words, i am looking for the native equivalent of:</p>
  
  <pre><code>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&amp;3", "contoso");
  </code></pre>
  
  <h1>Hasn't this been asked, and answered, to death?</h1>
  
  <p>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. </p>
  
  <p>Microsoft managed to solve it in .NET with the <strong>PrincipalContext</strong> class added in .NET 3.5. And the <strong>PrincipalContext</strong> is nothing magical. Underneath it uses the flat C-style <code>ldap</code> 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.</p>
  
  <h1>What have you tried?</h1>
  
  <h2><strong>Method 1</strong>: <em>Just use <code>LogonUser</code></em></h2>
  
  <p>I cannot use <code>LogonUser</code>. LogonUser works only when your machine is either <strong>on</strong> the domain you're validating (e.g. <code>contoso</code>) or your domain <em>trusts</em> the domain you're validating. In other words, if there is a domain controller out there for the <strong>contoso.test</strong> domain then:</p>
  
  <pre><code>LogonUser("iboyd", "contoso.test", "Tr0ub4dor&amp;3", 
        LOGON32_LOGON_NETWORK, "Negotiate", ref token);
  </code></pre>
  
  <p>will fail with error:</p>
  
  <blockquote>
    <p>1326 (Logon failure: unknown user name or bad password) </p>
  </blockquote>
  
  <p>That's because this domain i specify is not my own domain, or a domain i trust. </p>
  
  <blockquote>
    <p>C# <code>PrincipalContext</code> doesn't suffer from this problem.</p>
  </blockquote>
  
  <h2><strong>Method 2</strong>: <em>Just use the SSPI (Security Support Provider Interface)</em></h2>
  
  <p>The SSPI is what <code>LogonUser</code> uses internally. The short answer is that it fails for the same reason that <code>LogonUser</code> does: Windows will not trust credentials from an untrusted domain.</p>
  
  <p>The code is pretty long to provide an example of. The psuedo-code jist is:</p>
  
  <pre><code>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
  </code></pre>
  
  <p>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.</p>
  
  <blockquote>
    <p>C# <code>PrincipalContext</code> doesn't suffer from this problem.</p>
  </blockquote>
  
  <h2><strong>Method 3</strong>: <em>Just use LDAP's <a href="https://msdn.microsoft.com/en-us/library/aa772184.aspx" rel="noreferrer">AdsGetObject</a></em></h2>
  
  <p>Some might suggest using <a href="https://msdn.microsoft.com/en-us/library/aa772184%28v=vs.85%29.aspx?f=255&amp;MSPPError=-2147217396" rel="noreferrer"><code>AdsGetObject</code></a>. </p>
  
  <pre><code>AdsGetObject("LDAP://CN=iboyd,DC=contoso,DC=test");
  </code></pre>
  
  <p>That's a red-herring, because <code>AdsGetObject</code> supports no way to pass username/password:</p>
  
  <pre><code>HRESULT ADsGetObject(
    _In_   LPCWSTR lpszPathName,
    _In_   REFIID riid,
    _Out_  VOID **ppObject
  );
  </code></pre>
  
  <p>Instead you will be simply asking about a user. </p>
  
  <p>Perhaps you meant <code>AdsOpenObject</code>:</p>
  
  <pre><code>HRESULT ADsOpenObject(
    _In_   LPCWSTR lpszPathName,
    _In_   LPCWSTR lpszUserName,
    _In_   LPCWSTR lpszPassword,
    _In_   DWORD dwReserved,
    _In_   REFIID riid,
    _Out_  VOID **ppObject
  );
  </code></pre>
  
  <p>where you <strong>can</strong> specify credentials to connect as.</p>
  
  <blockquote>
    <p>C# <code>PrincipalContext</code> doesn't suffer from this problem.</p>
  </blockquote>
  
  <h2><strong>Method 4</strong>: <em>Just use <code>AdsOpenObject</code></em></h2>
  
  <p>Some might suggest using <a href="https://msdn.microsoft.com/en-us/library/aa772238%28v=vs.85%29.aspx?f=255&amp;MSPPError=-2147217396" rel="noreferrer"><code>AdsOpenObject</code></a>:</p>
  
  <pre><code>String path = "LDAP://CN=iboyd,DC=contoso,DC=test"
  AdsOpenObject(path, "iboyd", "Tr0ub4dor&amp;3", 0, IADs, ref ads);
  </code></pre>
  
  <p>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:</p>
  
  <ul>
  <li><em>{username}</em></li>
  <li><em>{password}</em></li>
  <li><em>{domain}</em></li>
  </ul>
  
  <p>that is because</p>
  
  <blockquote>
    <p>LDAP://CN=<code>{username}</code>,DC=<code>{domain}</code></p>
  </blockquote>
  
  <p>is not a valid LDAP path for any user.</p>
  
  <p>Notwithstanding the LDAP path conundrum, the fundamental issue is that trying to <strong>query</strong> LDAP. That is wrong.</p>
  
  <p>We want to <strong>validate</strong> a user's AD credentials. </p>
  
  <p>Any time we attempt to <strong>query</strong> an LDAP server, we will fail if the user does not have permission to query LDAP - even though their credentials are valid. </p>
  
  <p>When you pass credentials to <code>AdsOpenObject</code> they are used to specify who you want to <em>connect</em> as. Once you connect, you will then perform a query against  LDAP. When you don't have permission to query LDAP, the <strong>AdsOpenObject</strong> will fail.</p>
  
  <p>What's even more maddening is that even if you <strong>do</strong> have permission to query for users in LDAP, you're still needlessly performing a query of LDAP - an expensive operation.</p>
  
  <blockquote>
    <p>C# <code>PrincipalContext</code> doesn't suffer from this problem.</p>
  </blockquote>
  
  <h2><strong>Method 5</strong>: <em>Use ADO with the ADsDSOObject provider</em></h2>
  
  <p>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</p>
  
  <pre><code>String sql = 
      'SELECT userAccountControl FROM "LDAP://DC=contoso,DC=test"
      'WHERE objectClass="user" 
      'and sAMAccountName = "iboyd"';
  
  String connectionString = "Provider=ADsDSOObject;Password=Tr0ub4dor&amp;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);
  </code></pre>
  
  <p>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.</p>
  
  <p>Plus there's the issue that it is querying Active Directory, when it should be validating credentials.</p>
  
  <blockquote>
    <p>C# <code>PrincipalContext</code> doesn't suffer from this problem.</p>
  </blockquote>
  
  <h2><strong>Method 6</strong>: <em>Just use PrincipalContext.ValidateCredentials</em></h2>
  
  <p>The .NET 3.5 class <code>PrincipalContext</code> has that lets you validate credentials knowing only:</p>
  
  <ul>
  <li>Username</li>
  <li>Password</li>
  <li>Domain Name</li>
  </ul>
  
  <p>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 <strong>don't need permission to query Active Directory</strong> - it just works.</p>
  
  <p>I tried digging down into the source code using ILSpy, but it gets harry fast:</p>
  
  <pre><code>ValidateCredentials
     CredentialValidator.Validate
        BindLdap
           new LdapDirectoryIdentifier
           new LdapConnection
           ldapConnection.SessionOptions.FastConcurrentBind();
              lockedLdapBind
                  Bind
  </code></pre>
  
  <p>With a <strong>lot</strong> 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 <em>wrote</em> code that calls it!</p>
  
  <p><strong>Note</strong>: 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.</p>
  
  <p>Unfortunately:</p>
  
  <pre><code>System.Security.DirectoryServices.AccountManagement.PrincipalContext
  </code></pre>
  
  <p>was not exposed though a COM-callable wrapper:</p>
  
  <p><img src="https://i.stack.imgur.com/zq8Za.png" alt="enter image description here"></p>
  
  <p>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.</p>