Friday, January 28, 2005

Determining if a user is locked out using LDAP in .NET

Experienced ADSI users might know the following fun fact about determining if a user is locked out in Active Directory: the 'userAccountControl' attribute is not the place to look. The 'userAccountControl' is a bunch of flags that determine a lot of settings on the user (or related class type) object. Here are a list of flags associated with it:

const int UF_SCRIPT = 0x0001;
const int UF_ACCOUNTDISABLE = 0x0002;
const int UF_HOMEDIR_REQUIRED = 0x0008;
const int UF_LOCKOUT; = 0x0010
const int UF_PASSWD_NOTREQD = 0x0020;
const int UF_PASSWD_CANT_CHANGE = 0x0040;
const int UF_TEMP_DUPLICATE_ACCOUNT = 0x0100;
const int UF_NORMAL_ACCOUNT = 0x0200;
const int UF_INTERDOMAIN_TRUST_ACCOUNT = 0x0800;
const int UF_WORKSTATION_TRUST_ACCOUNT = 0x1000;
const int UF_SERVER_TRUST_ACCOUNT = 0x2000;
const int UF_DONT_EXPIRE_PASSWD = 0x10000;
const int UF_MNS_LOGON_ACCOUNT = 0x20000;

It would seem intuitive that you should use the UF_LOCKOUT and check to see if that flag was set on the 'userAccountControl'. Of course, that only works if you are using the WinNT provider. The next logical solution would be to .Invoke the ADSI 'AccountIsLocked' property using reflection. However, again, that won't work with the LDAP provider for the same reasons - internally it is using the UF_LOCKOUT flag. So the question becomes, how do I figure out if the account is locked out using the LDAP provider?

You can do this by a small calculation. You first need to inspect the user object's 'lockoutTime' attribute and determine if it is not '0' (meaning it was locked out, but reset). If it is not '0', then you need to covert that value to a DateTime object and calculate how much time has passed by inspecting the 'lockoutDuration' attribute on the 'domainDNS' class. This will tell you how long must pass before the account is automatically unlocked. Compare that to DateTime.Now and you can see if the account is still in the lockout period. Here is a sample:
private bool IsAccountLocked( DirectoryEntry user )
{
    //if they have a lockoutTime
    if (user.Properties.Contains("lockoutTime"))
    {
        long fileTicks = LongFromLargeInteger(user.Properties["lockoutTime"].Value);

        //check to see if it's not already unlocked
        if (fileTicks != 0)
        {
            //now check to see if it was automatically unlocked
            DateTime lockoutTime = DateTime.FromFileTime(fileTicks);

            DirectoryEntry parent = user.Parent;
            while (parent.SchemaClassName != "domainDNS")
                parent = parent.Parent;

            long durationTicks = LongFromLargeInteger(parent.Properties["lockoutDuration"].Value);
            
            return (DateTime.Now.CompareTo(lockoutTime.AddTicks(-durationTicks)) < 0);
        }
    }
    return false;
}

//decodes IADsLargeInteger objects into a FileTime format (long)
private long LongFromLargeInteger( object largeInteger )
{
    System.Type type = largeInteger.GetType();
    int highPart = (int)type.InvokeMember("HighPart",BindingFlags.GetProperty, null, largeInteger, null);
    int lowPart = (int)type.InvokeMember("LowPart",BindingFlags.GetProperty, null, largeInteger, null);

    return (long)highPart << 32 | (uint)lowPart;
}
The tricky part comes in when you need to basically recurse back to the domain object to find the 'lockoutDuration'. Of course, if you knew the 'lockoutDuration', and knew it would not change, you could skip this step and make the comparison directly.