Friday, 15 July 2005

Determining if an account is locked out in .NET revisited.

There is an interesting email conversation going on over on the ADSI listgroup regarding the best way to determine if an account is locked out.  One of the contributors has pointed out that Microsoft has updated the schema in ADAM and Windows 2003 to include a new constructed attribute called ‘msDS-User-Account-Control-Computed’.  This attribute can accurately reflect the UF_LOCKOUT flag, unlike the standard ‘userAccountControl’ using the LDAP provider.  The question comes out, which is the better method?

From my last post on this topic, I introduced a method of determining if an account was locked out when we only have the user’s DirectoryEntry.  This method works on all platforms, including Windows 2000.  It was correctly pointed out that this particular code also had some shortcomings.  Key amongst these were that it used recursion to find the ‘lockoutDuration’ for the domain.  I never liked that bit of code and I originally hesitated to use it.  I did it anyway just so users could see where it was located.  Now, I am going to revisit this again and perhaps show a better way of finding locked accounts that works on all platforms.

//get this from RootDSE or other...
string adsPath = _defaultNamingSyntax;

//Explicitly create our SearchRoot
DirectoryEntry searchRoot = new DirectoryEntry(
    adsPath,
    null,
    null,
    AuthenticationTypes.Secure
    );

//default for when accounts stay locked indefinitely
string qry = "(lockoutTime>=1)";

if (searchRoot.Properties.Contains("lockoutDuration"))
{
    long lockoutDuration = LongFromLargeInteger(
        searchRoot.Properties["lockoutDuration"].Value
        );

    DateTime lockoutThreshold = DateTime.Now.AddTicks(lockoutDuration);

    Console.WriteLine(
        "Threshold Lockout: {0}",
        lockoutThreshold.ToString()
        );

    qry = String.Format(
        "(lockoutTime>={0})",
        lockoutThreshold.ToFileTime()
        );
}

using (searchRoot)
{
    DirectorySearcher ds = new DirectorySearcher(
        searchRoot,
        qry
        );

    using (SearchResultCollection src = ds.FindAll())
    {
        Console.WriteLine(qry);

        foreach (SearchResult sr in src)
        {
            Console.WriteLine(
                "{0} locked out at {1}",
                sr.Properties["name"][0],
                DateTime.FromFileTime((long)sr.Properties["lockoutTime"][0])
                );
        }
    }
}

 

//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;
}

This is just sample code here, but it should give you an idea of what to do.  I put in a lot of .WriteLine statements to hopefully make clear what is occurring.  This dynamically determines the lockout duration policy and performs a simple search to determine which users are still locked out.  This is done is only one call to the directory and is pretty efficient (add more indices to the filter and it gets better).  If you were looking for only one user, you could obviously add that into the query and depending on whether or not you got a result back, you would know if they were locked out!

Here are the caveats:

  • Domain time skew can throw you off.  If you need millisecond precision this might not be for you.  If you can live with a few minutes drift depending on the local time where the user was locked out (any more and Kerberos would have some issues as would replication) then this is darn accurate.
  • I have not tested this against ADAM – assuming that ADAM keeps its ‘lockoutDuration’ on the root of the application partition and you have set the defaultNamingContext (or manually point to it) it should work fine.

Now, what about using the ‘msDS-User-Account-Control-Computed’ attribute?  It works great when you have the DirectoryEntry and you know that you are using ADAM or Windows 2003.  It is also decisive, i.e. if the bit is flipped you are locked out - no questions.  It has the following limitations however:

  • Limited Platform support (no Windows 2000)
  • Cannot be searched for directly since it is constructed attribute (bummer!)
  • Requires a second call to the directory in a .RefreshCache() for each object to inspect since it is constructed.  If you are checking lockout status a lot this adds up quickly.

So… which one should you use?  That is completely up to you.  Keep in mind the limitations of each and just pick one.

Standard Disclaimer:  In the event this does not work for you… or utterly destroys your machine, I take no responsibility.  This is sample code and not production ready.  It has been through only limited testing, so test it yourself as well.

Friday, 28 January 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.