Skip to main content

Windows Last Logon problem and solution

As someone who has been involved in Network Administration (in Microsoft Land) since Windows NT 4.0, I find it surprising that is still so difficult to get simple (yet important) information such as "When was the last time Joe User logged in?".

One would think that, with the fourth edition of Active Directory in production (Windows Server 2008 R2), a tool or set of tools would have been issued with Windows to provide those answers.  Well, because they don't, I've decided to go ahead and write one.  (Yes, I know that there are probably others out there to be downloaded or purchased but... you know, I don't care.)

All that is required: PowerShell 2.0 (and a functioning Active Directory).

Defining the Problem:
Active Directory stores numerous properties on objects in the Directory. Some of these properties are replicated amongst the Domain Controllers and some are not. Unfortunately, for some reason, one of the design decisions was to not replicate the "LastLogon" property for Computer and User accounts. So, for example, if I log in with username "sam" on DomainController01 then the LastLogon property is updated on that Domain Controller.  However, DomainController02 has no record of when I've logged on and will never know, unless I happen to authenticate against it at some point.

This is not a problem if an organization has only one Domain Controller (as is often the case in small shops), but in the event of a shop with two or more, it can be quite misleading. For example, many administrators will use the "net user" command to see when the last time a user logged onto the network was:


Unfortunately, the time-stamp highlighted in red will only be accurate if the user being checked on happened to authenticate to the same Domain Controller that I authenticated to. This is because the "net.exe" command will inquire against the currently set LogonServer (which can be viewed by typing "set logonserver" from a command line).

Therefore: the "net user" command, with regard to "Last Logon", cannot be trusted in environments with more than one Domain Controller.


Defining the Solution:


So, what is required to actually get the /true/ last logon?  There are two solutions:

1). Crawl through all of the event logs on all of the servers. I don't particularly care for this option.
2). Basically one has to query each Domain Controller and compare the answers, with the newest one being authoritative. See the flow chart below.

Note: Some folks would argue for the "LastLogonTimestamp" property to be queried because it is replicated between DCs. This doesn't work though, because that is designed to find old accounts that are generally inactive, not for immediate (and exactly accurate) results. (It can be as much as 14 days incorrect - see: The Technet Blog article.)


Diagram of solution 2:

This will ensure that we get the latest value for LastLogon.

To accomplish the above task, I've written a little Powershell "Advanced Function". It gathers the names of all of the Domain Controllers in the organization, then queries each of them a la the flow chart above. In accepts Pipeline input in the form of "username" or "sAMAccountName".

Example usages:


The screen shot above, I give a couple of examples. The first one, I simply pass in a samAccountName (or "username").  The second, I pass in an array of usernames, which the script will process in order. Finally, in the 3rd example, I use the Quest ActiveRoles ADManagement snapin (free download) to pipe the output of "Get-QADUser" to the script. This is to illustrate that the get-LastUserLogon accepts pipeline input.

The script is as follows:
#============= COPY BELOW THIS LINE =============

<#
    .Synopsis
        get-LastUserLogon will return the last logon date of a user from a Microsoft
        Windows domain environment.
    
    .Description
        get-LastLogon will query all of the domain controllers and find the most
        recent LastLogon time. This function is required because Active Directory
        does not replicate that information between Domain Controllers.
   
    .Parameter user
        The User parameter is a list of sAMAccountNames (common usernames) to query for
        last logon time.
        
    .Inputs
        This will receive a property called "User" from the Pipeline and use that
        as basis for querying Active Directory.
    
    .Outputs
        Produces an object that has the following properties:
            [String] User
            [DateTime] LastLogon
            [String] LastLogonDC
#>


#requires -version 2.0


[CmdletBinding()]
param(
    [Parameter(
        ValueFromPipelineByPropertyName = $True,
        Position = 0,
        Mandatory = $True,
        HelpMessage="Username (sAMAccountName) or Usernames to process.")
    ]
    [Alias("samAccountName")]
    [String[]]
    $user
)


begin {
    $DCs = ([System.DirectoryServices.ActiveDirectory.Domain]::getcurrentdomain()).DomainControllers
    $domDN = (New-Object System.DirectoryServices.DirectoryEntry).distinguishedName
}


process {
    $user | ForEach-Object {
        $currentUser = $_
        $lastLogon = [DateTime]0
        $lastLogonDC = ""
        $lastLogonObject = New-Object Object
        $DCs | ForEach-Object {
            $currentDC = $_.name
            $strFilter = "(&(objectCategory=Person)(samAccountName=$currentUser))"
            $objDomain = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$currentDC/$domDN")
            $objSearcher = New-Object System.DirectoryServices.DirectorySearcher
            $objSearcher.SearchRoot = $objDomain
            $objSearcher.PageSize = 1000
            $objSearcher.Filter = $strFilter
            $objSearcher.SearchScope = "Subtree"
            $colResults = $objSearcher.FindAll()            
            
            $colResults | foreach-object {
                try {
                    $currentLastLogonVal = [datetime]::FromFileTime([int64]::Parse($_.Properties.lastlogon))
               
                    if($currentLastLogonVal -gt $lastLogon) {
                        $lastLogon = $currentLastLogonVal
                        $lastLogonDC = $currentDC
                    }
                } catch {


                }
            }
        }
        
        [String]$lastLogonString = ""
        if($lastLogon -ne [DateTime]0) {
            $lastLogonString = $lastLogon
        } else {
            $lastLogonString = "Never"
        }
        $lastLogonObject | Add-Member noteproperty "LastLogon" -Value $lastLogonString
        $lastLogonObject | Add-Member noteproperty "Username" -value $currentUser
        $lastLogonObject | Add-Member noteproperty "LastLogonDC" -value $lastLogonDC
        
        Write-Output $lastLogonObject
    }
}


end {


}
#============= END COPY ABOVE THIS LINE =============


This script is fully functional and I use it in production. There isn't a huge amount of error handling, but feel free to add it if you are so inclined. :)

I hope someone finds this valuable.

gsamuelhays

Comments

Popular posts from this blog

Automating IP addresses with TMG

In the past few months I've had the "opportunity" (note the quotes, maybe I'll write a post about all the troubles I've run into) to deal with some Office 365 integration projects.

When integrating one or more cloud services with Office 365 or Office 365 with on-prem Exchange, it is typical to limit the IP addresses that are aloud to communicate between services. This is an important layer on the security model and obviously reduces the attack surface. Microsoft maintains lists of IP addresses for the various services here:

http://onlinehelp.microsoft.com/en-us/office365-enterprises/hh373144.aspx

That page links to other pages with more specific information about various other Microsoft cloud services such as the link below for Exchange online.

http://help.outlook.com/en-us/140/gg263350.aspx for example.

At the time of this writing, you'll notice that the lists are inconsistent between pages. >:-(

Microsoft on two separate Premier Support Calls has told me…

LDAP and SAN Certificates

Hey everyone - I ran across an interesting problem with certificates and services. The problem was that I needed to see which certificate is being presented to the client on a non-HTTPS service; specifically - binding to LDAP over SSL (LDAPS). So the question is, how can you be sure what certificate is being presented?

Windows, so far as I've been able to find, does not offer any native help in this regard. Luckily there is a solution, but first let me give you a slightly longer description of the scenario so you can appreciate what we're talking about a little bit better.

An environment exists with some number of Domain Controllers, let's say ten. Within this environment, third party applications (think Java apps, Linux systems, etc) need to bind to LDAP to enumerate groups or validate authentication or whatever. These systems, however, can only have a single (or at best, two) LDAP hosts configured. What do you do? Pick two DCs? Round-Robin DNS?

Well, in my case, a load-b…