- Published on
How to Find Account Expiration Dates in Active Directory (PowerShell Script)
Account expiration is the setting everyone forgets exists until a contractor's access outlives their contract, or an intern account expires mid-project and nobody knows why they suddenly can't log in. Unlike password expiration, AccountExpirationDate is a hard cutoff — once it passes, the account can't authenticate at all, no matter how recent the password is.
The quick answer
Get-ADUser -Filter * -Properties AccountExpirationDate | Where-Object { $_.AccountExpirationDate }
A more useful reporting script
#requires -Modules ActiveDirectory
<#
.SYNOPSIS
Reports Active Directory user accounts that have an expiration date set.
#>
[CmdletBinding()]
param(
[Nullable[int]]$Days,
[string]$SearchBase,
[string]$OutputCsv
)
Import-Module ActiveDirectory -ErrorAction Stop
$now = Get-Date
$params = @{
Filter = { Enabled -eq $true }
Properties = @('AccountExpirationDate', 'DistinguishedName')
}
if ($SearchBase) { $params['SearchBase'] = $SearchBase }
$candidates = Get-ADUser @params | Where-Object { $_.AccountExpirationDate }
if ($Days) {
$cutoff = $now.AddDays($Days)
$candidates = $candidates | Where-Object { $_.AccountExpirationDate -le $cutoff }
}
$results = foreach ($user in $candidates) {
[PSCustomObject]@{
Name = $user.Name
SamAccountName = $user.SamAccountName
AccountExpirationDate = $user.AccountExpirationDate
Status = if ($user.AccountExpirationDate -lt $now) { 'Expired' } else { 'Active' }
DaysUntilExpiry = [Math]::Round(($user.AccountExpirationDate - $now).TotalDays)
DistinguishedName = $user.DistinguishedName
}
}
if (-not $results) {
Write-Host "No accounts found with an expiration date set (in scope)." -ForegroundColor Yellow
return
}
$sorted = $results | Sort-Object AccountExpirationDate
Write-Host "Found $(@($sorted).Count) account(s) with an expiration date set." -ForegroundColor Cyan
$sorted | Format-Table Name, SamAccountName, AccountExpirationDate, Status, DaysUntilExpiry -AutoSize
$expiredCount = @($sorted | Where-Object { $_.Status -eq 'Expired' }).Count
if ($expiredCount -gt 0) {
Write-Host "$expiredCount account(s) are already past their expiration date." -ForegroundColor Red
}
if ($OutputCsv) {
$sorted | Export-Csv -Path $OutputCsv -NoTypeInformation
Write-Host "Exported results to $OutputCsv" -ForegroundColor Green
}
-Days 14 filters the report to accounts expiring within two weeks (already-expired accounts always show up regardless), which is the useful view for a weekly "what's about to lose access" check.
Why account expiration and password expiration aren't the same thing
They're independent settings and it trips people up constantly. AccountExpirationDate is a hard cutoff on the account itself. Password expiration only forces a password change — it doesn't stop the account from logging on once a new password is set. An account can have either, both, or neither, and having one set says nothing about the other. If you're specifically after passwords about to expire rather than the account itself, Find-ExpiringADPasswords.ps1 in the same script repo (linked below) handles that instead.
The GUI method (no PowerShell)
In ADUC, the Account tab of a user's Properties dialog shows Account expires under Account options, but only for the one user you have open — there's no built-in saved query for "show me every account with an expiration date," which is exactly the gap this script fills.
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/Get-ADAccountExpiryReport.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.