This is a follow up to my previous question.
What is the correct way of validating the credentials passed to a
PrincipalContext
?
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)
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)
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:
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) {
}
User contributions licensed under CC BY-SA 3.0