views:

5515

answers:

5

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: 

The "password expires" check is relatively easy - at least on Windows (not sure how other systems handle this): when the Int64 value of "pwdLastSet" is 0, then the user will have to change his (or her) password at next logon. The easiest way to check this is include this property in your DirectorySearcher:

DirectorySearcher search = new DirectorySearcher(entry)
      { Filter = "(sAMAccountName=" + username + ")" };
search.PropertiesToLoad.Add("cn");
search.PropertiesToLoad.Add("pwdLastSet");

SearchResult result = search.FindOne();
if (result == null)
{
    return false;
}

Int64 pwdLastSetValue = (Int64)result.Properties["pwdLastSet"][0];

As for the "account is locked out" check - this seems easy at first, but isn't.... The "UF_Lockout" flag on "userAccountControl" doesn't do its job reliably.

Beginning with Windows 2003 AD, there's a new computed attribute which you can check for: msDS-User-Account-Control-Computed.

Given a DirectoryEntry user, you can do:

string attribName = "msDS-User-Account-Control-Computed";
user.RefreshCache(new string[] { attribName });

const int UF_LOCKOUT = 0x0010;

int userFlags = (int)user.Properties[attribName].Value;

if(userFlags & UF_LOCKOUT == UF_LOCKOUT) 
{
   // if this is the case, the account is locked out
}

If you can use .NET 3.5, things have gotten a lot easier - check out the MSDN article on how to deal with users and groups in .NET 3.5 using the System.DirectoryServices.AccountManagement namespace. E.g. you now do have a property IsAccountLockedOut on the UserPrincipal class which reliably tells you whether or not an account is locked out.

Hope this helps!

Marc

marc_s
Thanks marc ... will try it out. Doesn't the System.DirectoryServices.AccountManagement in .NET 3.5 limit me to windows active directories though? Or does it still apply the same LDAP prinicipals?
Jabezz
Ah sorry - yes, S.DS.AM is Active Directory specific, sorry. But there's also a "low-level" LDAP library in namespace System.DirectoryServices.Protocols, since .NET 2.0 (I believe)
marc_s
Hi Marc, I tried out the suggestions, but keep running into the same issue. I can't even apply a DirectorySearcher if I pass the username/pwd into the DirectoryEntry constructor, as authentication will fail if account is locked out. If I don't pass it through I can do a search, but don't get to any of the mention properties. "pwdLastSet" is not there and user.Properties is always empty. Guess I'll have to place this on ice for a while.
Jabezz
@jabezz: well, the user that is locked out of course cannot log in and check his status - you'll have to have an admin log in and check that user's account.
marc_s
@marc : you are correct, however the authentication done in code, might cause the account to lock out. Thus it should first show the user that login was unsuccessful because of an invalid password, then when he tries to log in again, show him a message that his account has been locked out and he should contact an administrator. Otherwise he will just keep trying to log in if we just say the password is invalid.
Jabezz
@jabezz: I see and understand what you're trying to do - but as far as I can recall, there's no way to find that out. If the user is locked out, he can't log in to check if he's locked out :-( and the error message from the failed login isn't specific as to WHY he can't log in (wrong password or account locked out). You would have to have an admin console to log in to find out whether the user is indeed locked out or not.
marc_s
+3  A: 

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("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);
ScottBai
A: 

Hello everyone. I am facing the same problem and I am using the same code as provided by Jabezz. The code however, is part of a step by step ASP.Net guide on how to create a login page that authenticates with AD. The problem is that the code described in the question above is in a public bool function and it only returns true/false as values. All I need is to know if the password is expired or has never been set so I can create a condition on that to redirect users to reset their passwords. I know almost nothing about .NET coding and that is making it, as you can imagine, a lot more difficult than it is. Please help! This is frustrating me because I can't even find something if I wanted to pay for it.

Vince
A: 

I can't comment yet, but ScottBai's answer is right on for my needs. I tested the code snippets and they tell me exactly why the login fails. Thanks!

AltGeek
That will work yes, my problem is that for my purposes it needs to work on a Novel LDAP server as well. Then Win API is not going to work.
Jabezz
A: 

Here are the AD LDAP attributes that change for a user when a password is locked out (first value) versus when a password is not locked out (second value). badPwdCount and lockoutTime are obviously the most relevant. I'm not sure whether uSNChanged and whenChanged must be updated manually or not.

$ diff LockedOut.ldif NotLockedOut.ldif:

< badPwdCount: 3
> badPwdCount: 0

< lockoutTime: 129144318210315776
> lockoutTime: 0

< uSNChanged: 8064871
> uSNChanged: 8065084

< whenChanged: 20100330141028.0Z
> whenChanged: 20100330141932.0Z
joeforker