- Published on
How to Find Users with Expiring Passwords in Active Directory (PowerShell Script)
Users find out their password is about to expire when Windows nags them at login — if they're lucky. There's no PasswordExpiryDate attribute sitting in AD waiting to be queried; the expiration date has to be calculated from PasswordLastSet plus your domain's MaxPasswordAge, and if fine-grained password policies (PSOs) are in play, different users can have different effective policies entirely.
Why there's no simple one-liner here
Unlike most of the scripts in this series, this one can't be a single Get-ADUser -Filter call — the expiration date isn't stored, it's derived per-user. The script below does that math for you.
The reporting script
#requires -Modules ActiveDirectory
<#
.SYNOPSIS
Lists Active Directory users whose password is about to expire.
.DESCRIPTION
Reads the domain's default password policy (or a fine-grained policy per
user, if one applies) and calculates each enabled user's password
expiration date from PasswordLastSet, then reports anyone expiring within
the given number of days. Accounts with PasswordNeverExpires set are
skipped since they have no expiration date. Uses the current logged-on
user's credentials — run it from a domain-joined machine with RSAT
installed.
.PARAMETER Days
Report users whose password expires within this many days. Defaults to 14.
.PARAMETER SearchBase
Distinguished name of the OU to search (e.g. "OU=Sales,DC=contoso,DC=com").
If omitted, searches the entire domain.
.PARAMETER OutputCsv
Path to a CSV file to export the results to. If omitted, results are only
written to the console.
.EXAMPLE
.\Find-ExpiringADPasswords.ps1
Lists users whose password expires within the next 14 days.
.EXAMPLE
.\Find-ExpiringADPasswords.ps1 -Days 30 -OutputCsv .\expiring-passwords.csv
Lists users whose password expires within 30 days and exports to CSV.
#>
[CmdletBinding()]
param(
[int]$Days = 14,
[string]$SearchBase,
[string]$OutputCsv
)
Import-Module ActiveDirectory -ErrorAction Stop
$defaultPolicy = Get-ADDefaultDomainPasswordPolicy
$now = Get-Date
$cutoff = $now.AddDays($Days)
$params = @{
Filter = { Enabled -eq $true -and PasswordNeverExpires -eq $false }
Properties = @('PasswordLastSet', 'PasswordNeverExpires', 'DistinguishedName')
}
if ($SearchBase) {
$params['SearchBase'] = $SearchBase
}
$candidates = Get-ADUser @params
$results = foreach ($user in $candidates) {
if (-not $user.PasswordLastSet) {
continue
}
$finePolicy = Get-ADUserResultantPasswordPolicy -Identity $user -ErrorAction SilentlyContinue
$maxAge = if ($finePolicy) { $finePolicy.MaxPasswordAge } else { $defaultPolicy.MaxPasswordAge }
if (-not $maxAge -or $maxAge -eq [TimeSpan]::Zero) {
continue
}
$expiresOn = $user.PasswordLastSet + $maxAge
if ($expiresOn -le $cutoff) {
[PSCustomObject]@{
Name = $user.Name
SamAccountName = $user.SamAccountName
PasswordLastSet = $user.PasswordLastSet
ExpiresOn = $expiresOn
DaysUntilExpiry = [Math]::Round(($expiresOn - $now).TotalDays)
DistinguishedName = $user.DistinguishedName
}
}
}
if (-not $results) {
Write-Host "No passwords expiring within $Days day(s)." -ForegroundColor Yellow
return
}
$sorted = $results | Sort-Object ExpiresOn
Write-Host "Found $(@($sorted).Count) password(s) expiring within $Days day(s)." -ForegroundColor Cyan
$sorted | Format-Table Name, SamAccountName, ExpiresOn, DaysUntilExpiry -AutoSize
if ($OutputCsv) {
$sorted | Export-Csv -Path $OutputCsv -NoTypeInformation
Write-Host "Exported results to $OutputCsv" -ForegroundColor Green
}
Checking one user's actual effective policy
If fine-grained password policies are in use, Get-ADUserResultantPasswordPolicy tells you which one actually applies to a specific person — the default domain policy unless a PSO with higher precedence targets them directly or a group they're in:
Get-ADUserResultantPasswordPolicy -Identity jdoe
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.
- Password and expiry reporting — Accounts with "password never expires", passwords older than your policy, and accounts that never set one at all.
- Scheduled reports by email — Daily, weekly, or monthly runs delivered to the right inbox automatically. No Task Scheduler job for you to babysit.
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-ExpiringADPasswords.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.