Click here to Skip to main content
15,867,488 members
Articles / Programming Languages / C#
Article

Find LastLogon Across All Windows Domain Controllers

Rate me:
Please Sign up or sign in to vote.
3.65/5 (9 votes)
14 Jun 2007CPOL4 min read 113K   27   24
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.

C#
// 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.

C#
// 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.

C#
// 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

C#
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:

C#
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).

C#
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.

C#
// 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.

C#
// 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)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralWorks for Computers Pin
JohnALewis13-Dec-17 5:45
JohnALewis13-Dec-17 5:45 
QuestionGreat post! It works for me! Pin
Van Thi7-Oct-15 22:12
Van Thi7-Oct-15 22:12 
QuestionHow to use this code? Pin
Alex Markessinis7-Aug-13 7:20
Alex Markessinis7-Aug-13 7:20 
QuestionBug Pin
Cribs1-Nov-12 8:23
Cribs1-Nov-12 8:23 
GeneralImprovements to the code in this article Pin
Chris128928-Jul-10 12:59
Chris128928-Jul-10 12:59 
GeneralRe: Improvements to the code in this article Pin
Rajesh Thampi14-Jan-15 0:27
Rajesh Thampi14-Jan-15 0:27 
GeneralRe: Improvements to the code in this article Pin
Dwight Johnson14-Jan-15 2:52
Dwight Johnson14-Jan-15 2:52 
GeneralRe: Improvements to the code in this article Pin
Rajesh Thampi14-Jan-15 4:42
Rajesh Thampi14-Jan-15 4:42 
GeneralNot working out newest Pin
Still_Learning_it_all27-Nov-08 14:05
Still_Learning_it_all27-Nov-08 14:05 
Generalexception Pin
omer nauman21-Jul-08 22:30
omer nauman21-Jul-08 22:30 
GeneralPaged searches Pin
JeffTz11-Jun-08 21:53
JeffTz11-Jun-08 21:53 
GeneralScans every DC in every domain of the forest Pin
odessamax6-Jun-08 13:04
odessamax6-Jun-08 13:04 
When i ran this logic in a multi domain environment, it was scanning every Domain Controller in EVERY DOMAIN in a forest !!!
So, instead of scanning just 5 DCs of one particular domain it was scaning all DCs in all 15 domains.

To scan one particular domain I am using:

DirectoryContext context = new DirectoryContext(DirectoryContextType.Domain,domain);
DomainControllerCollection dcc = DomainController.FindAll(context);
foreach (DomainController dc in dcc)
{
....
}
GeneralRe: Scans every DC in every domain of the forest Pin
Dwight Johnson9-Jun-08 2:59
Dwight Johnson9-Jun-08 2:59 
GeneralRe: Scans every DC in every domain of the forest Pin
odessamax10-Jun-08 10:57
odessamax10-Jun-08 10:57 
GeneralRe: Scans every DC in every domain of the forest Pin
Dwight Johnson10-Jun-08 10:59
Dwight Johnson10-Jun-08 10:59 
GeneralRe: Scans every DC in every domain of the forest Pin
RichWeber3-Dec-08 14:02
RichWeber3-Dec-08 14:02 
GeneralRe: Scans every DC in every domain of the forest Pin
RichWeber4-Dec-08 14:13
RichWeber4-Dec-08 14:13 
GeneralFair warning Pin
John Storer II19-Jun-07 2:34
John Storer II19-Jun-07 2:34 
GeneralRe: Fair warning Pin
Dwight Johnson19-Jun-07 7:22
Dwight Johnson19-Jun-07 7:22 
GeneralRe: Fair warning Pin
John Storer II19-Jun-07 7:38
John Storer II19-Jun-07 7:38 
GeneralRe: Fair warning Pin
Dwight Johnson19-Jun-07 9:41
Dwight Johnson19-Jun-07 9:41 
GeneralRe: Fair warning Pin
John Storer II19-Jun-07 9:48
John Storer II19-Jun-07 9:48 
GeneralRe: Fair warning Pin
johnhamm23-Oct-09 4:43
johnhamm23-Oct-09 4:43 

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

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