In an Active Directory environment, probably the most reliable way to query the last logon time of a computer is to use the Last-Logon attribute. The Last-Logon-Timestamp attribute could be used, but this will not likely be up-to-date due to the replication lag. If you are using PowerShell, the LastLogonDate attribute can also be used, however this is also a replicated attribute which suffers from the same delay and potential inaccuracy.
The Last-Logon attribute is not replicated, however, it is only stored on the DC that the computer authenticated against. If you have multiple domain controllers, you will get multiple values for this attribute depending on which DC the computer has authenticated with and when.
To find the Last-Logon date from the DC that the computer has most recently authenticated with, you need to query all domain controllers for this attribute, then select the most recent.
Following is a PowerShell script I wrote that will read a list of domain controllers from an Active Directory OU, query each one, then return the most recent Last-Logon value. It uses parallel processing to return the result more quickly than processing each DC in turn, which is useful in a multi-DC environment.
To use the script, simply pass the computer name and optionally the AD OU containing your domain controllers, to the function. You can hard-code the ‘DomainControllersOU’ parameter in the script if you prefer, so you don’t need to call it. You need the Active Directory module installed to use this.
Example
Get-ADLastLogon -ComputerName PC001 -DomainControllersOU "OU=Domain Controllers,DC=contoso,DC=com"
Get-ADLastLogon
function Get-ADLastLogon { [CmdletBinding()] Param ( [Parameter(Mandatory=$false, ValueFromPipelineByPropertyName=$true, Position=0)] [string]$DomainControllersOU = "OU=Domain Controllers,DC=contoso,DC=com", [string] $ComputerName ) # Multithreading function function Invoke-InParallel { [CmdletBinding()] param( [parameter(Mandatory = $True,ValueFromPipeline=$true,Position = 0)] $InputObject, [parameter(Mandatory = $True)] [ScriptBlock]$Scriptblock, [string]$ComputerName, [parameter()] $ThrottleLimit = 32, [parameter()] [switch]$ShowProgress ) Begin { # Create runspacepool, add code and parameters and invoke Powershell [void][runspacefactory]::CreateRunspacePool() $SessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() $script:RunspacePool = [runspacefactory]::CreateRunspacePool(1,$ThrottleLimit,$SessionState,$host) $RunspacePool.Open() # Function to start a runspace job function Start-RSJob { param( [parameter(Mandatory = $True,Position = 0)] [ScriptBlock]$Code, [parameter()] $Arguments ) if ($RunspacePool.GetAvailableRunspaces() -eq 0) { do {} Until ($RunspacePool.GetAvailableRunspaces() -ge 1) } $PowerShell = [powershell]::Create() $PowerShell.runspacepool = $RunspacePool [void]$PowerShell.AddScript($Code) foreach ($Argument in $Arguments) { [void]$PowerShell.AddArgument($Argument) } $job = $PowerShell.BeginInvoke() # Add the job and PS instance to the arraylist $temp = '' | Select-Object -Property PowerShell, Job $temp.PowerShell = $PowerShell $temp.Job = $job [void]$Runspaces.Add($temp) } # Start a 'timer' $Start = Get-Date # Define an arraylist to add the runspaces to $script:Runspaces = New-Object -TypeName System.Collections.ArrayList } Process { # Start an RS job for each computer $InputObject | ForEach-Object -Process { Start-RSJob -Code $Scriptblock -Arguments $_, $ComputerName } } End { # Wait for each script to complete foreach ($item in $Runspaces) { do { } until ($item.Job.IsCompleted -eq 'True') } # Grab the output from each script, and dispose the runspaces $return = $Runspaces | ForEach-Object -Process { $_.powershell.EndInvoke($_.Job) $_.PowerShell.Dispose() } $Runspaces.clear() [void]$RunspacePool.Close() [void]$RunspacePool.Dispose # Stop the 'timer' $End = Get-Date $TimeTaken = [math]::Round(($End - $Start).TotalSeconds,2) # Return the results $return } } # Get list of domain controllers from OU try { Import-Module ActiveDirectory | out-null $DomainControllers = Get-ADComputer -Filter * -SearchBase $DomainControllersOU -Properties Name -ErrorAction Stop | Select -ExpandProperty Name | Sort } catch {} # Define Code to run in each parallel runspace $Code = { param($DC,$ComputerName) Import-Module ActiveDirectory | out-null $Date = [datetime]::FromFileTime((Get-ADComputer -Identity $ComputerName -Server $DC -Properties LastLogon | select -ExpandProperty LastLogon)) $Result = '' | Select 'Domain Controller','Last Logon' $Result.'Domain Controller' = $DC $Result.'Last Logon' = $Date Return $Result } # Run code in parallel $Result = Invoke-InParallel -InputObject $DomainControllers -Scriptblock $Code -ComputerName $ComputerName -ThrottleLimit 64 # Return most recent logon date return $Result | sort 'Last Logon' -Descending | select -First 1 }
Hi,
you script is very interesting but i try to run the script at win10 and get no output and no errors 😦
Hi Oliver,
It works well for me on Windows 10. Do you have the Active Directory module / RSAT tools installed?
Hi Trevor,
yes i have. I have try many way to run the script. But i get no errors and no result.
I have hard-code the ‘DomainControllersOU’ in the script and run the script with the machinename.
Possible a idea for the next version from this script: Check not only one machine, check a ou and export a list with the data for the ou.
Try putting a $_ in the catch block, you should get an error message then if it is failing to read the list of DCs from the OU.
Hi, How do you retrieve the similar info for user. I have tried get-Aduser but not luck
Never tried that tbh
I know this is an older post, but this is exactly what I was looking for. How can I run this for multiple machines and add the machine name to the results?