Google
 

12.05.2005

Active Directory and lastlogontimestamp

The Problem
I was tasked with retrieving all of the enabled Machines accounts from Active Directory and the date/time they last logged in. After googling some articles on the topic, I found some reasonable solutions, but ran into a couple issues on the topic so I thought I’d document it here for others.

To start off, here is the one article you want to read to introduce the topic and give some background:
Dandelions, VCR Clocks, and Last Logon Times: These are a Few of Our Least Favorite Things

Most of the references online show how to retrieve the lastlogontimestamp using the DirectoryEntry object, as illustrated in the next snippet of code I pulled from one post on a forum showing how to properly retrieve the lastlogontimestamp value:

What the post failed to do was explain what any of this meant.
The lastlogontimestamp is stored in Active Directory as object that implements the IADsLargeInteger (an ActiveDS object). Ideally, AD would return a long (Int64) instead of the IADsLargeInteger object, eliminating the need to reference COM (ActiveDS is where this interface is defined), as well as allowing use to work with a .NET primitive.

Instead we have to use the IADsLargeInteger interface to extract the HighPart, stored as an Int32, shift it left 32 and cast it to a long (Int64) and then OR that with the LowPart, another Int32. If you’re wondering what the bit shifting and OR ‘’ code is doing – it essentially converts the IADsLargeInteger into a long (Int64).

Now let’s say you are using the DirectorySearcher object instead of the DirectoryEntry object to find the object in Active Directory you need. If you try to use similar code as provided in the sample above, you will get an InvalidCastException when casting the lastlogontimestamp to the IADsLargeInteger. For some reasone, when using the DirectorySearcher object, the lastlogontimestamp object is returned as a long (Int64) instead of an IADsLargeInteger. I found this behavior to be a bit schizophrenic.

Here’s an example of what I’m talking about:

A Poorly Named Method
The next thing you may be wondering is why I show an examples that use the DateTime.FromFileTime() method. What does a file time have to do with Active Directory and the lastlogontimestamp?

Well, I’m guessing whoever wrote the .NET DateTime object must have thought something like this – “a time expressed in a 100 nanosecond units starting from January 1, 1601 is how file times are stored. I’ll write a method on DateTime called FromFileTime(long nanotime)” or maybe “AH crap, I’ve got this file timestamp as a long and I need an easy way to convert it to a DateTime object without repeating the same code over and over again. I’ll just add a method to DateTime that takes a long and returns the corresponding DateTime.”

This method name, while accurate, is a limited expression of what is expressed as a 100 nanosecond unit starting from 1/1/1601- namely ANY time that is expressed as an Int64. Active Directory uses this same 100 nanosecond unit to store its timestamps and I’m better there are other things that do as well.

What IS nice about the FromFileTime() method is that it returns a DateTime using the current Time Zone, so we don’t have to convert it.

There are actually a couple ways to convert the Int64 100 nano second representation to the DateTime object in .NET and they are as follows:

1. One of the TimeSpan object constructor overloads takes a period expressed in 100 nanosecond units which is exactly what AD is returning, if we create a DateTime object that starts at 1/1/1601, and then add a TimeSpan created with our Int64 value, we should get the corresponding UTC time expressed in a DateTime object. Finally we need to convert the UTC time to local time. Here is the code to illustrate this:



2. More simply, we can just use the DateTime.FromFileTime() and this will accomplish what the code above does, though with less code.
Here’s some sample code:

With all of that said, the solution I came up with to report on the lastlogontimestamp of the machine accounts in AD is pretty straight forward:

One Last Problem:
It turns out, the value of lastlogontimestamp is replicated from the DC's to the GC every 14 days. This ends up presenting a problem if you want real-time data. If you really want the latest and greatest value logon time, the solution is as follows: Connect to EACH domain controller and check the lastlogon value instead. I wrote code to do this and found that on a LARGE domain with over 4,000 computers the time it takes to run this report takes a loooooong time to run. If anyone has any suggestions on how to practically do this efficiently, please let me know.

Further reading:
Decimal Time - Computers – a reference describing how Computers store times in Decimal.

10 comments:

Eric said...

Monty,

Nice information. I was able to rapidly diagnose an active directoy problem using your:

dictionaryEntry.Properties.Containts["lastLogontimestamp"][0];
as opposed to
object lastTime = dictionaryEntry.Properties["lastLogonTimestamp"].Value;

The value object was not propery being intialized.

excellent work :)

j. montgomery said...

glad to hear it!

brainstew said...

How exactly would you query each DC to get the lastlogin? We only have two DCs and 100 computers so this shouldn't be too taxing.

j. montgomery said...

Here's one way:

1. Query AD for all users you're interested.

2. Loop over the Search Results and for each user, query each DC for their most current data using the Distinguished Name field.

3. Prefix the Distinguished Name with the DC Server dns name like so:

E.g.
_directoryEntry1 = new DirectoryEntry("LDAP://DC1.BLAH.COM/CN=Monty\, J,OU=Users,OU=IT,DC=BLAH,DC=COM");
_directoryEntry2 = new DirectoryEntry("LDAP://DC2.BLAH.COM/CN=Monty\, J,OU=Users,OU=IT,DC=BLAH,DC=COM");

4. Then extract the lastlogon time stamp for each directory entry:

e.g.
_entryDc1.Properties["lastlogontimestamp"]
_entryDc2.Properties["lastlogontimestamp"]

5. Compare the results to see which value is more current and use the most recent timestamp.

brainstew said...

Thanks a lot! I can't believe I couldn't get such an easy thing before.

Great blog, keep it up.

j. montgomery said...

Thanks! Glad you find it helpful.

mr.mojo said...

I found a (somewhat, still has to query every DC, but is compact function) better way of doing this.

DirectoryContext context = new DirectoryContext(DirectoryContextType.Domain, LDAP_PATH_TO_DC);

DateTime latestLogon = DateTime.MinValue;
DomainControllerCollection dcc = DomainController.FindAll(context);

foreach (DomainController dc in dcc)
{
DirectorySearcher ds = dc.GetDirectorySearcher();

ds.Filter = "(sAMAccountName=" + USERNAME + ")";
ds.PropertiesToLoad.Add("lastlogon");
ds.SizeLimit = 1;

SearchResult sr = ds.FindOne();
ds.Dispose();

if (sr != null)
{
if (sr.Properties["lastLogon"].Count > 0)
{
DateTime lastLogon = DateTime.FromFileTime((long)sr.Properties["lastLogon"][0]);
if (lastLogon > latestLogon)
latestLogon = lastLogon;
}
}
}
}
}

Not incredible. But a pretty neat way of doing it.

j. montgomery said...

Nice! Thanks for sharing.

Anonymous said...

Question! We only have one domain controller and it is only pulling the initial login timestamp. Why would we have to wait 14 days if there is no replication occurring?

j. montgomery, CISSP, GNET said...

> Question! We only have one
> domain controller and it
> is only pulling the initial
> login timestamp. Why would we
> have to wait 14 days if there
> is no replication occurring?

Ahh, interesting. I hadn't considered just one DC, though I suspect it's fairly common. I suspect (I don't know for sure) that the value should be pretty up-to-date then...if you're able to verify this, let me know!