- Published on
How to Find Account Expired Users in Active Directory (PowerShell Script)
An account expiring is supposed to be the end of the story, but it isn't always — expiry blocks new logons, it doesn't disable the account or end a session that's already running. Finding accounts that are both expired and still enabled is one of the more actionable findings in a routine access review, because it's exactly the kind of gap that shouldn't exist but quietly does.
The quick answer
Search-ADAccount -UsersOnly -AccountExpired
A more useful reporting script
#requires -Modules ActiveDirectory
<#
.SYNOPSIS
Lists Active Directory user accounts whose expiration date has already passed.
#>
[CmdletBinding()]
param(
[string]$SearchBase,
[string]$OutputCsv
)
Import-Module ActiveDirectory -ErrorAction Stop
$params = @{ UsersOnly = $true; AccountExpired = $true }
if ($SearchBase) { $params['SearchBase'] = $SearchBase }
$expiredUsers = Search-ADAccount @params |
Get-ADUser -Properties AccountExpirationDate, DistinguishedName |
Select-Object Name, SamAccountName, Enabled, AccountExpirationDate, DistinguishedName |
Sort-Object AccountExpirationDate
if (-not $expiredUsers) {
Write-Host "No expired user accounts found." -ForegroundColor Yellow
return
}
Write-Host "Found $(@($expiredUsers).Count) expired user account(s)." -ForegroundColor Cyan
$expiredUsers | Format-Table Name, SamAccountName, Enabled, AccountExpirationDate -AutoSize
$stillEnabledCount = @($expiredUsers | Where-Object Enabled).Count
if ($stillEnabledCount -gt 0) {
Write-Host "$stillEnabledCount expired account(s) are still Enabled — expiry alone doesn't disable an account." -ForegroundColor Red
}
if ($OutputCsv) {
$expiredUsers | Export-Csv -Path $OutputCsv -NoTypeInformation
Write-Host "Exported results to $OutputCsv" -ForegroundColor Green
}
-SearchBase scopes this to a specific OU — the Contractors OU is the obvious candidate, since that's usually where time-boxed accounts with an expiration date live in the first place.
Why "expired" doesn't mean "harmless"
Account expiry only blocks new interactive logons going forward. It doesn't terminate a session that's already active, and it doesn't disable the account the way Disable-ADAccount does — the account is still, technically, enabled, and any process already running under it (a scheduled task, a service, a cached token) keeps working until something else stops it. That's exactly why the script flags "still enabled" separately instead of treating every expired account the same.
Turning an expired account off for good
Search-ADAccount -UsersOnly -AccountExpired | Where-Object Enabled | Disable-ADAccount
Review the list first (or run with -WhatIf) before pointing a bulk operation like this at production.
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.
- Scheduled reports by email — Daily, weekly, or monthly runs delivered to the right inbox automatically. No Task Scheduler job for you to babysit.
- 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/Find-ADAccountExpiredUsers.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.