Currently I authenticate users against some AD using the following code:
DirectoryEntry entry = new DirectoryEntry(_path, username, pwd);
try
{
// Bind to the native AdsObject to force authentication.
Object obj = entry.NativeObject;
DirectorySearcher search = new DirectorySearcher(entry) { Filter = "(sAMAccountName=" + username + ")" };
search.PropertiesToLoad.Add("cn");
SearchResult result = search.FindOne();
if (result == null)
{
return false;
}
// Update the new path to the user in the directory
_path = result.Path;
_filterAttribute = (String)result.Properties["cn"][0];
}
catch (Exception ex)
{
throw new Exception("Error authenticating user. " + ex.Message);
}
This works perfectly for validating a password against a username.
The problem comes in that a generic errors is always returned "Logon failure: unknown user name or bad password." when authentication fails.
However authentication might also fail when an account is locked out.
How would I know if it is failing because of it being locked out?
I've come across articles saying you can use:
Convert.ToBoolean(entry.InvokeGet("IsAccountLocked"))
or do something like explained here
The problem is, whenever you try to access any property on the DirectoryEntry, the same error would be thrown.
Any other suggestion of how to get to the actual reason that authentication failed? (account locked out / password expired / etc.)
The AD I connect to might not neccesarily be a windows server.
A little late but I'll throw this out there.
If you want to REALLY be able to determine the specific reason that an account is failing authentication (there are many more reasons other than wrong password, expired, lockout, etc.), you can use the windows API LogonUser. Don't be intimidated by it - it is easier than it looks. You simply call LogonUser, and if it fails you look at the Marshal.GetLastWin32Error() which will give you a return code that indicates the (very) specific reason that the logon failed.
However, you're not going to be able to call this in the context of the user you're authenticating; you're going to need a priveleged account - I believe the requirement is SE_TCB_NAME (aka SeTcbPrivilege) - a user account that has the right to 'Act as part of the operating system'.
//Your new authenticate code snippet:
try
{
if (!LogonUser(user, domain, pass, LogonTypes.Network, LogonProviders.Default, out token))
{
errorCode = Marshal.GetLastWin32Error();
success = false;
}
}
catch (Exception)
{
throw;
}
finally
{
CloseHandle(token);
}
success = true;
if it fails, you get one of the return codes (there are more that you can look up, but these are the important ones:
//See http://support.microsoft.com/kb/155012
const int ERROR_PASSWORD_MUST_CHANGE = 1907;
const int ERROR_LOGON_FAILURE = 1326;
const int ERROR_ACCOUNT_RESTRICTION = 1327;
const int ERROR_ACCOUNT_DISABLED = 1331;
const int ERROR_INVALID_LOGON_HOURS = 1328;
const int ERROR_NO_LOGON_SERVERS = 1311;
const int ERROR_INVALID_WORKSTATION = 1329;
const int ERROR_ACCOUNT_LOCKED_OUT = 1909; //It gives this error if the account is locked, REGARDLESS OF WHETHER VALID CREDENTIALS WERE PROVIDED!!!
const int ERROR_ACCOUNT_EXPIRED = 1793;
const int ERROR_PASSWORD_EXPIRED = 1330;
The rest is just copy/paste to get the DLLImports and values to pass in
//here are enums
enum LogonTypes : uint
{
Interactive = 2,
Network =3,
Batch = 4,
Service = 5,
Unlock = 7,
NetworkCleartext = 8,
NewCredentials = 9
}
enum LogonProviders : uint
{
Default = 0, // default for platform (use this!)
WinNT35, // sends smoke signals to authority
WinNT40, // uses NTLM
WinNT50 // negotiates Kerb or NTLM
}
//Paste these DLLImports
[DllImport("advapi32.dll", SetLastError = true)]
static extern bool LogonUser(
string principal,
string authority,
string password,
LogonTypes logonType,
LogonProviders logonProvider,
out IntPtr token);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool CloseHandle(IntPtr handle);