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.