Friday, September 9, 2011

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