Click here to Skip to main content
Click here to Skip to main content

Find LastLogon Across All Windows Domain Controllers

, 14 Jun 2007
Rate this:
Please Sign up or sign in to vote.
Use the .Net Framework to find the lastLogon for every user across all domain controllers

Introduction

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.

Background

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.

// Get the root entry
DirectoryEntry rootDSE = new DirectoryEntry("LDAP://RootDSE");
string configurationNamingContext = 
    (string)rootDSE.Properties["configurationNamingContext"].Value;
string defaultNamingContext = 
    (string)rootDSE.Properties["defaultNamingContext"].Value;
// Get all the domain controllers

The RootDSE path gets the root entry for the whole domain. Here, I must capture two values: configurationNamingContext and 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]

Using configurationNamingContext, you can now process all of the users from each of the domain controllers in your domain.

// Get all the domain controllers
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 = 
            deDomain.Parent.Properties["DNSHostName"].Value.ToString();
        // Get all the users for that domain
    }
}

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.

// Get all the users for that domain
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) 
    {
        // Get the distinguishedName and lastLogon for each user
        // Save the most recent logon for each user in a Dictionary object
    }
}

Now we are finally getting down to the nitty-gritty. Notice that the path here includes two previously captured pieces of information: DNSHostName and 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 Dictionary object.

References and the dictionary object

using System;            
    // has DateTime class
using System.Collections.Generic;    
    // has the Dictionary class
using System.DirectoryServices;    
    // has all the LDAP classes such as DirectoryEntry 
using ActiveDs;            
    // has the IADsLargeInteger class

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 distinguishedName and 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 lastLogon (value).

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 distinguishedName and lastLogon in the Dictionary. On the first domain controller, you will just be putting each distiguishedName and 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. lastLogons[distinguishedName], where 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.

// Get the distinguishedName and lastLogon for each user
string distinguishedName = 
    deUser.Properties["distinguishedName"].Value.ToString();
Int64 lastLogonThisServer = new Int64();
if (deUser.Properties["lastLogon"].Value != null) 
{
    IADsLargeInteger lgInt = 
        (IADsLargeInteger)deUser.Properties["lastLogon"].Value;
    lastLogonThisServer = ((long)lgInt.HighPart << 32) + lgInt.LowPart;
}

// Save the most recent logon for each user in a Dictionary object
if (lastLogons.ContainsKey(distinguishedName)) 
{
    if (lastLogons[distinguishedName] < lastLogonThisServer) 
    {
        lastLogons[distinguishedName] = lastLogonThisServer;
    }
} 
else 
{
    lastLogons.Add(distinguishedName, lastLogonThisServer);
}

By the way, please note the tricky little calculation that is required to convert the date and time information from IADsLargeInteger into Int64. The date is stored in the HighPart and the time in the LowPart.

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.

// Convert the long integer to a DateTime value
string readableLastLogon = 
    DateTime.FromFileTime(lastLogonThisServer).ToString();

Acknowledgments

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.

Conclusion

The Active Directory is a vital part of Windows networks. I hope this article makes dealing with it a bit easier.

History

  • 14 June, 2007 -- Original version posted

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Dwight Johnson

United States United States
No Biography provided

Comments and Discussions

 
QuestionHow to use this code? PinmemberAlex Markessinis7-Aug-13 7:20 
QuestionBug PinmemberCribs1-Nov-12 8:23 
GeneralImprovements to the code in this article PinmemberChris128928-Jul-10 12:59 
GeneralNot working out newest PinmemberStill_Learning_it_all27-Nov-08 14:05 
Generalexception Pinmemberomer nauman21-Jul-08 22:30 
GeneralPaged searches PinmemberFarquar de St.John11-Jun-08 21:53 
GeneralScans every DC in every domain of the forest Pinmemberodessamax6-Jun-08 13:04 
GeneralRe: Scans every DC in every domain of the forest PinmemberDwight Johnson9-Jun-08 2:59 
GeneralRe: Scans every DC in every domain of the forest Pinmemberodessamax10-Jun-08 10:57 
GeneralRe: Scans every DC in every domain of the forest PinmemberDwight Johnson10-Jun-08 10:59 
GeneralRe: Scans every DC in every domain of the forest PinmemberRichWeber3-Dec-08 14:02 
GeneralRe: Scans every DC in every domain of the forest PinmemberRichWeber4-Dec-08 14:13 
GeneralFair warning PinmemberJohn Storer II19-Jun-07 2:34 
GeneralRe: Fair warning PinmemberDwight Johnson19-Jun-07 7:22 
GeneralRe: Fair warning PinmemberJohn Storer II19-Jun-07 7:38 
GeneralRe: Fair warning PinmemberDwight Johnson19-Jun-07 9:41 
GeneralRe: Fair warning PinmemberJohn Storer II19-Jun-07 9:48 
GeneralRe: Fair warning Pinmemberjohnhamm23-Oct-09 4:43 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140814.1 | Last Updated 14 Jun 2007
Article Copyright 2007 by Dwight Johnson
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid