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?
You could just move the actual usage of the context into the
try
block: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 (installSystem.DirectoryServices
from NuGet). You'll see from the stack trace thatPrincipalContext
usesDirectoryEntry
underneath anyway. I've found that usingDirectoryEntry
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
:Another way is to use
LdapConnection
directly (installSystem.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.