For the sake of enhanced security at my company, I was asked to develop a way to determine the last time all of the company employees logged into our network. This isn't as easy as you might think. There are generally multiple servers in the domain and the lastLogon value for any one individual user could be different on each of those servers. I was familiar with the System.DirectoryServices namespace, but there were other issues, as you will see.
One of the network analysts at my company gave me a VBScript he had found that loads a Dictionary object with this exact information. Richard L. Mueller of Hilltop Lab generously provides this script free-of-charge on his website. See the Acknowledgements section below for links to the script itself. It seemed that doing the same thing in C# using the .NET Framework should be a trivial task. Much did, in fact, convert easily from VBScript to C#. However, there were a few missing pieces that took me a good deal of time to figure out. Hence, this article.
The root of the matter
The first step is to locate the root
DirectoryEntry for your domain.
DirectoryEntry rootDSE = new DirectoryEntry("LDAP://RootDSE");
string configurationNamingContext =
string defaultNamingContext =
RootDSE path gets the root entry for the whole domain. Here, I must capture two values:
defaultNamingContext. The first value will be used immediately in the next step and the second will be required further along.
Master of every domain [server]
configurationNamingContext, you can now process all of the users from each of the domain controllers in your domain.
DirectoryEntry deConfig =
new DirectoryEntry("LDAP://" + configurationNamingContext);
DirectorySearcher dsConfig = new DirectorySearcher(deConfig);
dsConfig.Filter = "(objectClass=nTDSDSA)";
foreach (SearchResult srDomains in dsConfig.FindAll())
DirectoryEntry deDomain = srDomains.GetDirectoryEntry();
if (deDomain != null)
string dnsHostName =
Let's go through the key issues here. First, you can see that you use
configurationNamingContext, captured in the root entry in the previous step, as your LDAP path. To find the domain controllers, you need to filter using
(objectClass=nTDSDSA). Then, for each domain controller you find, you need to look for the
DNSHostName of its
Parent entry. Once you get to this point, you're ready to get the
lastLogons for all of the users on that domain controller.
Users are persons
Yes, I know, you may not always think so. But according to the Active Directory, all users are persons. At least, that's how you have to search for them.
DirectoryEntry deUsers =
new DirectoryEntry("LDAP://" + dnsHostName + "/" + defaultNamingContext);
DirectorySearcher dsUsers = new DirectorySearcher(deUsers);
dsUsers.Filter = "(&(objectCategory=person)(objectClass=user))";
foreach (SearchResult srUsers in dsUsers.FindAll())
DirectoryEntry deUser = srUsers.GetDirectoryEntry();
if (deUser != null)
Now we are finally getting down to the nitty-gritty. Notice that the path here includes two previously captured pieces of information:
defaultNamingContext. This last one was been captured way back at the root entry. (See, I told you we would eventually need it.) We are now going to capture two pieces of information for each user: their
distinguishedName and their
lastLogon. Once we have those, we'll add them to the
Dictionary object. Before we get to that, though, I must backtrack a bit and talk about references and the
References and the dictionary object
First, the references. If you are going to convert
lastLogon to a readable
DateTime object, you'll need the
System namespace that is included by default in most classes anyway. The
System.Collections.Generic namespace contains the
Dictionary class, which will be used in the next step to capture the
lastLogon data for each user. The
distinguishedName value is used because it uniquely identifies each user. For example, here's what mine looks like:
CN=Dwight Johnson, OU=Corporate, DC=preitllc, DC=com
lastLogon is a tricky beast. It is stored as an
IADsLargeInteger object in the active directory. However, to add the reference for that class, you must select the COM tab and look for the Active DS Type library. This shows up in the References list as ActiveDs. Finally, to store the user's logon information, declare a
Dictionary object. Since it is a generic class, define it for a string to hold
distinguishedName (key) and for
Int64 to hold
Dictionary<string, Int64> lastLogons = new Dictionary<string, Int64>();
Save the latest of the last
Now it finally becomes clear why the logon information is being stored in a
Dictionary object, which I have named
lastLogons. As you roll through all of the users on each Domain Server, you store
lastLogon in the Dictionary. On the first domain controller, you will just be putting each
lastLogon into a new key / value pair in the Dictionary. However, when processing all of the users on the next and subsequent controllers, you will likely run into
distinguishedNames that you've already stored. In those cases, you want to compare the
lastLogon previously stored -- i.e.
distinguishedName is the key and the
lastLogon is the value -- with the
lastLogon of the current server,
lastLogonThisServer. The most recent one -- i.e. the latest of the last -- will be saved.
string distinguishedName =
Int64 lastLogonThisServer = new Int64();
if (deUser.Properties["lastLogon"].Value != null)
IADsLargeInteger lgInt =
lastLogonThisServer = ((long)lgInt.HighPart << 32) + lgInt.LowPart;
if (lastLogons[distinguishedName] < lastLogonThisServer)
lastLogons[distinguishedName] = lastLogonThisServer;
By the way, please note the tricky little calculation that is required to convert the date and time information from
Int64. The date is stored in the
HighPart and the time in the
What time is it?
Finally, if you want to see
lastLogon as a
DateTime value, there is a method in the
System namespace that will do this.
string readableLastLogon =
I would like to thank Yann LeGuen, who brought the original VBScript to my attention. I would also like to again thank Richard L. Mueller for the excellent script that provided so much useful information.
The Active Directory is a vital part of Windows networks. I hope this article makes dealing with it a bit easier.
- 14 June, 2007 -- Original version posted