Building logic into PowerShell functions to nag users before their Active Directory password expires

This week I'm sharing a couple of PowerShell functions that are a work in progress to nag those users who seem to never want to change their passwords. I can't tell you how many times the help desk staff at one of the companies that I provide support for receives a call from a user who is unable to access email or other resources on the intranet.

The problem? They have run their password down to the point where they arrive in the morning, log into their computer without issue, and during the day while they're working their password expires which cuts them off from Intranet resources such as email and websites that require authentication.

If they happen to call me, I know what they've done and my first question to them is "Are you sure that you still work here? Maybe you were terminated and HR cut off your access to network resources."

An automated password expiration email reminder is already sent out daily, but users seem to ignore it so I want to use PowerShell to send one email per day 14 days out and send one email per hour during business hours when their password is going to expire within 3 days.

Use at your own risk. The functions shown in this blog article are a work in process and have not been thoroughly tested.

The first function simply retrieves the password expiration information in case I simply want a report of what's going to expire (and I don't want two functions with redundant code in them):

 1#Requires -Modules ActiveDirectory
 2function Search-MrADAccountPasswordExpiration {
 4    [CmdletBinding()]
 5    param (
 6        [ValidateNotNullOrEmpty()]
 7        [int]$Days = 14,
 9        [ValidateNotNullOrEmpty()]
10        [int]$MaximumPasswordAge = 90,
12        [ValidateNotNullOrEmpty()]
13        [string]$SearchBase = 'OU=Test Users,OU=Users,DC=mikefrobbins,DC=com'
14    )
16    [datetime]$CutoffDate = (Get-Date).AddDays(-($MaximumPasswordAge - $Days))
18    Get-ADUser -Filter {
19        Enabled -eq $true -and PasswordNeverExpires -eq $false -and PasswordLastSet -lt $CutoffDate
20    } -Properties PasswordExpired, PasswordNeverExpires, PasswordLastSet, Mail -SearchBase $SearchBase |
21    Where-Object PasswordExpired -eq $false |
22    Select-Object -Property Name, SamAccountName, Mail, PasswordLastSet, @{label='PasswordExpiresOn';expression={$($_.PasswordLastSet).AddDays($MaximumPasswordAge)}}

In the previous function, you'll notice that I piped to Where-Object to filter out the accounts where the password was already expired. I couldn't for the life of me get the PasswordExpired property to work properly with the Filter parameter of Get-ADUser so hopefully you don't have a lot of accounts where the password is already expired and the user is still enabled in your environment.

You could also dynamically determine the maximum password age for your Active Directory domain using this function, but it's as slow as Christmas and assumes that you're not using fine grained password policies so I would just as well hard code the value as I did in the previous function as to use this method of determining it:

 1#Requires -Modules GroupPolicy
 2function Get-MrMaximumPasswordAge {
 4    [CmdletBinding()]
 5    param(
 6        [ValidateNotNullOrEmpty()]
 7        [string]$GPOName = 'Default Domain Policy'
 8    )
10    (([xml](Get-GPOReport -Name $GPOName -ReportType Xml)).GPO.Computer.ExtensionData.Extension.Account |
11    Where-Object name -eq MaximumPasswordAge).SettingNumber

This function sends the emails. The switch statement along with the range operator is where the magic happens:

 1#Requires -Version 3.0
 2function Send-MrADPasswordReminder {
 4    [CmdletBinding()]
 5    param(
 6        [Parameter(ValueFromPipeline)]
 7        [ValidateNotNullOrEmpty()]
 8        [pscustomobject]$Users,
10        [ValidateNotNullOrEmpty()]
11        [datetime]$Date = (Get-Date),
13        [ValidateRange(0,23)]
14        [int]$HourOfDay = 9,
16        [ValidateNotNullOrEmpty()]
17        [mailaddress]$From = '',
19        [ValidateNotNullOrEmpty()]
20        [string]$SmtpServer = '',
22        [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty
23    )
25    BEGIN {
27        $Params = @{
28            From = $From
29            SmtpServer = $SmtpServer
30            Subject = 'Password Expiration'
31        }
33        if($PSBoundParameters.Credential) {
34            $Params.Credential = $Credential
35        }
37    }
39    PROCESS {
41        foreach ($User in $Users) {
43            $Params.To = $User.Mail
45            $DaysUntilExpiration = (New-TimeSpan -Start $Date -End $($User.PasswordExpiresOn)).Days
47            switch ($DaysUntilExpiration) {
49                {$_ -in 4..14} {
50                    if ($Date.Hour -eq $HourOfDay) {
51                        Send-MailMessage @Params -Body "Your network password expires in $DaysUntilExpiration days. Please change your network password at your earliest possible convenience."
52                    }
53                    break
54                }
55                {$_ -in 0..3} {
56                    Send-MailMessage @Params -Body "Your network password expires in $DaysUntilExpiration days. Please change your network password immediately."
57                    break
58                }
60            }
62        }
64    }

So this can be setup as a PowerShell Scheduled Job or a scheduled task to run every hour on the hour during business hours:

1Search-MrADAccountPasswordExpiration | Send-MrADPasswordReminder

Based on how the script is written, the user will receive one email per day if their password is going to expire in 4 to 14 days. They will receive an email every hour that the scheduled job or task runs if their password is going to expire in 3 days or less. Once their password is expired, they will not receive any further emails.

Here's a pro tip from the trenches: If the user changed their password and daylight saving time has started or ended since then but before their password expires, the password expiration date calculated by adding the domain's maximum password age to the date/time when the user last changed their password will be one hour off.