How to validate the credentials passed to a `Princ

2019-08-06 03:12发布

问题:

This is a follow up to my previous question.

Question

What is the correct way of validating the credentials passed to a PrincipalContext?

Background

In my application I instantiate a PrincipalContext using PrincipalContext(ContextType, String, String, String). I have a number of integration tests that fail when the credentials are incorrect (or the supplied credentials are not for an admin) so I want to be able to catch this.

If the credentials are invalid PrincipalContext.ConnectedServer throws a System.DirectoryServices.DirectoryServicesCOMException, however this is not discovered until the first use of the PrincipalContext.

try
{
    PrincipalContext ctx = new PrincipalContext(ContextType.Domain, "my_domain.local", "wrong_username", "wrong_password");
}
catch (exception e)
{
    // This block is not hit
}

// `System.DirectoryServices.DirectoryServicesCOMException` raised here
using (UserPrincipal user = UserPrincipal.FindByIdentity(ctx, IdentityType.SamAccountName, samAccountName)) {}

Exception Details:

System.DirectoryServices.DirectoryServicesCOMException
  HResult=0x8007052E
  Message=The user name or password is incorrect.

  Source=System.DirectoryServices
  StackTrace:
   at System.DirectoryServices.DirectoryEntry.Bind(Boolean throwIfFail)
   at System.DirectoryServices.DirectoryEntry.Bind()
   at System.DirectoryServices.DirectoryEntry.get_AdsObject()
   at System.DirectoryServices.PropertyValueCollection.PopulateList()
   at System.DirectoryServices.PropertyValueCollection..ctor(DirectoryEntry entry, String propertyName)
   at System.DirectoryServices.PropertyCollection.get_Item(String propertyName)
   at System.DirectoryServices.AccountManagement.PrincipalContext.DoLDAPDirectoryInitNoContainer()
   at System.DirectoryServices.AccountManagement.PrincipalContext.DoDomainInit()
   at System.DirectoryServices.AccountManagement.PrincipalContext.Initialize()
   at System.DirectoryServices.AccountManagement.PrincipalContext.get_QueryCtx()
   at System.DirectoryServices.AccountManagement.Principal.FindByIdentityWithTypeHelper(PrincipalContext context, Type principalType, Nullable`1 identityType, String identityValue, DateTime refDate)
   at System.DirectoryServices.AccountManagement.Principal.FindByIdentityWithType(PrincipalContext context, Type principalType, IdentityType identityType, String identityValue)
   at System.DirectoryServices.AccountManagement.UserPrincipal.FindByIdentity(PrincipalContext context, IdentityType identityType, String identityValue)

What I tried

My initial thought was to check the credentials on creation, however if we reuse the PrincipalContext with different credentials we get a System.DirectoryServices.Protocols.LdapException.

PrincipalContext ctx = new PrincipalContext(ContextType.Domain, "my_domain.local", "correct_username", "correct_password");
if (ctx.ValidateCredentials("correct_username", "correct_password"))
{
    // `System.DirectoryServices.Protocols.LdapException` raised here
    using (UserPrincipal user = UserPrincipal.FindByIdentity(ctx, IdentityType.SamAccountName, different_user)) {}
}

Exception Details:

System.DirectoryServices.Protocols.LdapException
  HResult=0x80131500
  Message=The LDAP server is unavailable.

  Source=System.DirectoryServices.Protocols
  StackTrace:
   at System.DirectoryServices.Protocols.ErrorChecking.CheckAndSetLdapError(Int32 error)
   at System.DirectoryServices.Protocols.LdapSessionOptions.FastConcurrentBind()
   at System.DirectoryServices.AccountManagement.CredentialValidator.BindLdap(NetworkCredential creds, ContextOptions contextOptions)
   at System.DirectoryServices.AccountManagement.CredentialValidator.Validate(String userName, String password)
   at System.DirectoryServices.AccountManagement.PrincipalContext.ValidateCredentials(String userName, String password)

What is the correct approach?

Is there an accepted way to test this? Should I try to assign PrincipalContext.ConnectedServer to a local variable and catch an exception?

回答1:

You could just move the actual usage of the context into the try block:

try
{
    PrincipalContext ctx = new PrincipalContext(ContextType.Domain, "my_domain.local", "wrong_username", "wrong_password");
    using (UserPrincipal user = UserPrincipal.FindByIdentity(ctx, IdentityType.SamAccountName, samAccountName)) {}
}
catch (exception e)
{

}

If you're planning to use that context for other operations, then that's the only way I can see how to test the credentials.

But if your only goal is to validate the credentials, then you could use DirectoryEntry directly (install System.DirectoryServices from NuGet). You'll see from the stack trace that PrincipalContext uses DirectoryEntry underneath anyway. I've found that using DirectoryEntry directly is much, much faster anyway, although it can be more complicated to work with sometimes.

Here is how you would validate credentials with just DirectoryEntry:

var entry = new DirectoryEntry("LDAP://domain.local", "username", "password");
//creating the object doesn't actually make a connection, so we have to do something to test it
try {
    //retrieve only the 'cn' attribute from the object
    entry.RefreshCache(new[] {"cn"});
} catch (Exception e) {

}

Another way is to use LdapConnection directly (install System.DirectoryServices.Protocols from NuGet). This is probably the least amount of actual network traffic that has to happen to validate the credentials. But you may have to figure out the authentication method. By default it uses Negotiate, but if that doesn't work, you will have to use a different constructor and choose the authentication method manually.

var id = new LdapDirectoryIdentifier("domain.local");
var conn = new LdapConnection(id, new NetworkCredential("username", "password"));
try {
    conn.Bind();
} catch (Exception e) {

}