- Published on
How to Find a User's True Last Logon Time in Active Directory (PowerShell Script)
Every stale-account script — including ours — leans on LastLogonDate, and for good reason: it's a single, cheap, replicated attribute. But "replicated" is exactly what makes it untrustworthy when you need a precise answer. LastLogonDate only updates every few days by default, so it can say someone hasn't logged on in two weeks when they logged on yesterday — just not on the DC you happened to query, and not recently enough for the replicated copy to have caught up.
Why this needs a different approach
LastLogon (no "Date") is the accurate, real-time attribute — and it is deliberately not replicated. Each domain controller only knows about logons it personally authenticated. Getting the true answer means asking every DC and taking the most recent result.
#requires -Modules ActiveDirectory
<#
.SYNOPSIS
Finds a user's true last logon time by checking every domain controller.
.DESCRIPTION
The LastLogon attribute is NOT replicated between domain controllers —
each DC only knows about logons it personally handled. LastLogonDate
(replicated, but only updated a few times a day) is fine for casual
stale-account checks but can be off by days. This queries LastLogon on
every DC in the domain directly and returns the maximum, which is the
only way to get an accurate answer. Uses the current logged-on user's
credentials — run it from a domain-joined machine with RSAT installed —
and needs to be able to reach every DC over RPC.
.PARAMETER SamAccountName
The user's SAM account name (e.g. jdoe). Required.
.EXAMPLE
.\Get-ADUserLastLogonAllDCs.ps1 -SamAccountName jdoe
Checks every DC and reports jdoe's true last logon time, plus the
per-DC breakdown.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$SamAccountName
)
Import-Module ActiveDirectory -ErrorAction Stop
$user = Get-ADUser -Identity $SamAccountName -ErrorAction Stop
$domainControllers = Get-ADDomainController -Filter *
$perDcResults = foreach ($dc in $domainControllers) {
try {
$dcUser = Get-ADUser -Identity $user.DistinguishedName -Server $dc.HostName -Properties LastLogon -ErrorAction Stop
$lastLogon = if ($dcUser.LastLogon) { [DateTime]::FromFileTime($dcUser.LastLogon) } else { $null }
} catch {
Write-Warning "Could not query $($dc.HostName): $($_.Exception.Message)"
$lastLogon = $null
}
[PSCustomObject]@{
DomainController = $dc.HostName
LastLogon = $lastLogon
}
}
$trueLastLogon = $perDcResults |
Where-Object { $_.LastLogon } |
Sort-Object LastLogon -Descending |
Select-Object -First 1
Write-Host "Per-DC last logon for $($SamAccountName):" -ForegroundColor Cyan
$perDcResults | Sort-Object DomainController | Format-Table DomainController, LastLogon -AutoSize
if ($trueLastLogon) {
Write-Host "True last logon (max across all DCs): $($trueLastLogon.LastLogon) (on $($trueLastLogon.DomainController))" -ForegroundColor Green
} else {
Write-Host "No DC reported a last logon time for $SamAccountName — the account may have never logged on." -ForegroundColor Yellow
}
When this actually matters
For a routine stale-account sweep, LastLogonDate is fine — the whole point of that report is a cleanup queue, not a legal record. This script is for the cases where precision matters: confirming an account genuinely wasn't used during an incident window, settling a dispute about whether an offboarded employee logged in after their last day, or double-checking a privileged account flagged as stale by a report that only looked at the replicated value. It's slower (one query per DC) by design — that's the cost of an answer you can actually stand behind.
Where the manual approach runs out of road
A one-off script is fine for a single check. It starts to hurt once you actually need to run this regularly:
- No scheduling. Cron/Task Scheduler can run the script, but now you own the scheduling, the credentials it runs as, and what happens when it silently fails.
- No history. A CSV export is a snapshot. Was this the same five accounts as last month, or a growing list? A single export can't tell you.
- No distribution. Getting the report to the right people (security, IT ops, compliance) on a schedule means building that plumbing yourself.
- Multi-domain/multi-forest pain. Run it once per domain, reconcile the results yourself, and hope naming/OU conventions are consistent across all of them.
- No alerting. If something changes unexpectedly between runs — an account re-enabled, a privileged group gaining a member — nothing tells you until you happen to run the script again.
None of that is a PowerShell problem. It's what turns a script into a product.
What SysFlint AD does instead
Our SysFlint AD runs the same kind of discovery shown above automatically, on a schedule, entirely inside your own network. No agents on domain controllers, no data leaving your environment. It's free, forever.
- Stale and inactive account detection — Enabled accounts that nobody has logged into for 30, 60, or 90+ days — the ones that are still a live attack surface.
- Multi-domain and forest coverage — Enumerate every domain in the forest in one pass and get one consolidated report instead of one per domain.
If you're currently doing this with a script on a scheduled task, here's the honest comparison between the two, or go straight to the download page.
Get this script, and the rest of them
The script above is part of awesome-it-scripts, SysFlint's free, open-source library of PowerShell scripts for Active Directory and other IT-admin tasks — no signup, no catch, MIT licensed. Grab this one directly from active-directory/Get-ADUserLastLogonAllDCs.ps1, or browse the whole thing.
Find every disabled/locked-out/stale/privileged-account script we've published (plus the FAQ that goes with each one) in the repo's active-directory folder. Star it if it's useful — new scripts land there regularly.