Active Directory User Account Password Expiration Notifier

The "Kitchen Sink" ("Does Everything") of Active Directory user account password expiration notification scripts.

October 06, 2023

updated

12 mins

Read Time

# responses beta

Comments
Filed

PowerShell

collection

Source Code

posted by

Brian Johnson

Brian Johnson

Objective/Purpose

 

Windows Server or Azure Administrators often need to notify end-users about imminent or upcoming password expirations. This is especially true as companies intensify their security measures for complaince reasons or for enhanced security measures. A proactive approach will usually reduce the number of incoming helpdesk tickets for this seemingly very simple, yet often overlooked issue which plauges most IT departments.

There is, however, another persistent issue: users who don’t reset passwords in time. This problem becomes pronounced when high-ranking personnel, such as Directors, VP, or C-Level executives, can’t access the network during critical times, like during a holiday shutdown.

A very common workaround involves an IT administrator setting the problematic user account password to “never expires,” circumventing the defualt password policy in directories like Active Directory. This poses additional security risks.

Various scripts which aim to address this concern are available on platforms like the Microsoft Developer Network. Still, none met my personal specific requirements that were presented. For instance, none provided email notifications with logging features. Moreover, since my previous company used Google Workplace for emails, I required a script compatible with G Suite/Google Workplace and supported PowerShell 7.

Planned Scenarios

  • 30 days before password expiration -> Notify user.
  • 14 days (2 weeks) before expiration -> Notify user.
  • 7 days (1 week) before expiration -> Notify both user and administrator(s).
  • 3 days before expiration -> Send an URGENT/high-priority email to the user and notify the administrator.
  • 1 day before expiration -> Send an URGENT/high-priority email to the user and notify the administrator.
  • When the password has already expired -> Determine subsequent actions.

Notification Considerations

  • Link to instructions for early password change.
    • Link to specific specific service (i.e. Okta/OneLogin single-sign on platform)
    • Link to video tutorial demonstrating a password reset procedure.
    • Screenshots
  • Decide if notifications should be sent to the user’s manager or lead.
  • Include a link to the company’s password policy.
  • Provide a link to the company’s service-desk or helpdesk ticketing system.

Notably, very few methods exist for users to reset passwords post-expiration. Even renowned platforms like Okta or OneLogin face challenges here, particularly when authentication is federated to Active Directory. This federation often leads to lost visibility into passowrds whcih presents challenges with password policy enforcement.

Given the lack of comprehensive solutions, I developed a feature-rich PowerShell, which fixes “everything but the kitchen sink.”

Questions I Had During The Scripts Creation

  • What should the IT department know about user account password statuses? Are there any impending expirations or existing expirations unknown to the user?
  • Does the user know their password might expire during extended off-periods, such as the December holidays?
  • Are there barriers to the user changing their password?
  • Are there account issues post user onboarding, like never setting a personal password?
  • How can a user be proactive about password changes?
  • Is the user informed about changing passwords remotely or within the trusted local network?

It’s also important to recognize that there is a Microsoft-supported avenue for domain-joined users to reset their password, expired or not. However, unfortunately it’s part of the Remote Desktop Web Access (“RDWeb”) feature, part of the larger Terminal Services server role, which may require client access licensing, depending on your setup.

To summarize, the plethora of existing scripts fell short of my expectations, prompting me to create a more holistic “everything but the kitchen sink” solution. This one has a Kitchen Sink included, so you, as an administrator will never be left in the dark to support your senior-level executives.

Features

 

  • PowerShell 7.x compatible
  • Windows Server 2022 compatible
  • Reports on Active Directory user accounts with either of the following user account/password-related issues:
    • User accounts whose password is expiring during the time-frame indicated
    • User accounts whose password has expired
    • User accounts who are not able to change their password
    • User accounts who have never set their password
  • Optionally sends (copies) an admin (or group) when the user password has expired
  • Optionally sends a summary log of all users (with password issues) to an admin (or group)
  • Script output is color-coded for at-a-glance understanding
  • Designed to be run automatically by way of Scheduled Task

Logging and Debugging

 

  • Optionally enable debug, verbose and/or logging modes
  • Optionally can attach a summary log file and send to the admin email
  • Optionally can create and store a log files in the script’s directory of all activities

Installation and Usage

 

Coming soon!

Source Code

 

powershell
##### AD Password Expiry Notification Script                                                                #####
##### Version 1.1 - Brian Johnson                                                                           #####
##### Github Gist - https://gist.github.com/digitigradeit/516bb5340deb9dc3284b50c85d3df151                  #####

$ScriptStartTime = (Get-Date -f s)

# Debug mode - send email to administrator instead of user; also useful for debugging in case something doesn't work as expected.
$Debug = $true

# Writes information to the terminal regarding what is going on
$Verbose = $true

# Writes everything to a log file
$Logging  = $true

# Enables or disables emails from sending, with debug enabled, emails are sent to the admin instead of the user
$EmailsEnabled = $true
$EmailLogEnabled = $true

$LogDir = "C:\ScheduledScripts\PasswordExpiry\Logs\"
$LogDate = Get-Date -Format "yyyy-MM-dd_hh-mm"
$LogFilePrefix = "Password_Expiry_Log-"
$LogFile = $LogDir+$LogFilePrefix+$LogDate+".csv"

function Write-Log {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [ValidateSet('DEBUG','ERROR',"WARN","INFO","LOG")]
        [string]$Severity = 'LOG',
        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Message
    )
    
    $LogObj = [pscustomobject]@{
        Time = (Get-Date -f s)
        Severity = $Severity
        Message = $Message
    }
    
    if ($Logging -eq $true) {
        $LogObj | Export-Csv  -Append -Path $LogFile -NoTypeInformation -Force
    }

    switch ($Severity) {
        { ($_ -match 'WARN') } { Write-Host ($LogObj | Format-Table -HideTableHeaders | Out-String) -ForegroundColor DarkCyan }
        { ($_ -match 'ERROR') } { Write-Host ($LogObj | Format-Table -HideTableHeaders | Out-String) -ForegroundColor Red }        
        { ($_ -match 'DEBUG') -and ($Debug -eq $true) } { Write-Host ($LogObj | Format-Table -HideTableHeaders | Out-String) -ForegroundColor DarkYellow }
        { ($_ -match 'INFO') -and ($Verbose -eq $true) } { Write-Verbose ($LogObj | Format-Table -HideTableHeaders | Out-String) }
    }
}

# Email server information
$FromEmailAddress = "[email protected]"
$FromEmailAddressObject = new-object System.Net.Mail.MailAddress( "$FromEmailAddress", "Sender First Last Name")
$ReplyToEmailAddress = "[email protected]"
$AdminEmailAddress = "[email protected]"

$SmtpServer = "smtp.gmail.com"
$SmtpPort = "587"
$SmtpUsername = "[email protected]"
$SmtpPassword = "********"

# Credential obect created from config SmtpUsername and SmtpPassword
$SecurePassword = ConvertTo-SecureString $SmtpPassword -AsPlainText -Force
$CredentialObject = New-Object System.Management.Automation.PSCredential ($SmtpUsername, $SecurePassword)


function ValidEmail { 
    param([string]$Email)
    try {
        $null = [mailaddress]$Email
        return $true
    }
    catch {
        return $false
    }
}

Function Send-Email {
    Param (
        [Parameter(Mandatory=$true)][string]$SendEmailTo,
        [Parameter(Mandatory=$true)][string]$SendEmailSubject,
        [Parameter(Mandatory=$true)][string]$SendEmailBody
    )
   
    if (($EmailsEnabled -eq $True) ) {
        
        try {
            if ($Debug -eq $True) {
                $SendEmailTo = $AdminEmailAddress
            }
            Write-Log "INFO" "Sending email to: $SendEmailTo"
            Send-MailMessage -SmtpServer $SmtpServer -Port $SmtpPort -UseSsl -From $FromEmailAddressObject -Bcc $AdminEmailAddress -ReplyTo $ReplyToEmailAddress -To $SendEmailTo -Credential $CredentialObject -Subject $SendEmailSubject -BodyAsHtml -Body $SendEmailBody
            return [string]$SendEmailTo
        }
        Catch {
            Write-Log "ERROR" "Could not send e-mail: $($Error[0])"
            return [string]($Error[0])
        }
    } else {
        Write-Log "INFO" "Email sending disabled: would have sent $SendEmailTo with $SendEmailSubject"
        return [string]$SendEmailTo
    }
}

# How many days in advance should the user be notified
$NotifyDays = 30,14,7,1,0
$MaximumDays = ($NotifyDays|Measure-Object -Maximum).Maximum

##### Password Expiry - Start #####

if ($Logging -eq $true) {
    Write-Log "LOG" "=================== START ($ScriptStartTime) ==================="
}

if ($Debug -eq $true) {
    Write-Log "DEBUG" "Debug mode enabled"
}

if (($Logging -eq $true) -and ($Verbose -eq $true)) {
    Write-Log "INFO" "Logging to file $LogFile"
} 


# Active Directory calls
# FullTime and Contractors are sepasrate queries, can combine into one
$MaxPasswordAge = ((Get-ADForest -Current LoggedOnUser).Domains | ForEach-Object{ Get-ADDefaultDomainPasswordPolicy -Identity $_ }).MaxPasswordAge.Days
$FulltimeUsers = Get-ADUser -SearchBase 'OU=Fulltime,OU=Company,DC=company,DC=local' -filter {Enabled -eq $True -and PasswordNeverExpires -eq $False} –Properties *,"msDS-UserPasswordExpiryTimeComputed"
$ContractorUsers = Get-ADUser -SearchBase 'OU=Contractors,OU=Company,DC=company,DC=local' -filter {Enabled -eq $True -and PasswordNeverExpires -eq $False} –Properties *,"msDS-UserPasswordExpiryTimeComputed"
$AllUsers = $FulltimeUsers + $ContractorUsers
$AllUsers = $AllUsers | Select-Object -Property "samAccountName","givenname","surname","mail","PasswordLastSet","dn","CannotChangePassword", @{Name="PasswordExpiryTime";Expression={[datetime]::FromFileTime($_."msDS-UserPasswordExpiryTimeComputed")}}

$Today = Get-Date

$ActionedUsers = @()
foreach($User in ($AllUsers)) { 
    $UserUsername = $User.samaccountname
    $UserPasswordExpiresOn = $User.PasswordExpiryTime
    $UserPasswordLastSetOn = $User.PasswordLastSet
    $UserFirstName = $User.givenname
    $UserLastName = $User.surname
    $UserCannotChangePassword = $User.CannotChangePassword
    $UserEmailAddress = $User.mail
    $UserDn = $User.dn

    $ActionedUser = New-Object psobject
    $ActionedUser | Add-Member NoteProperty "First Name" -Value $UserFirstName
    $ActionedUser | Add-Member NoteProperty "Last Name" -Value $UserLastName
    $ActionedUser | Add-Member NoteProperty "Username" -Value $UserUsername
    $ActionedUser | Add-Member NoteProperty "E-mail Address" -Value $User.mail

    $EmailSentTo = $null

    if ($UserCannotChangePassword -eq $true) {
        # Users who have the bit "cannotchangepassword" specified will mess up the calculations for 'PasswordLastSet' 'PasswordExpiryTime'.
        $ActionedUser | Add-Member NoteProperty "Status" -Value "-user cannot change password-" -Force
        $ActionedUsers += $ActionedUser

        Write-Log "INFO" "$UserUsername is not able to change their network password. Please ununcheck this option in Active Directory."
        Write-log "INFO" "$UserUsername => Admin Insight: $UserDn"

        $EmailSubject = "[ADMIN ACTION REQUIRED] $UserUsername cannot change their password"
        $Body = "<p></p><p>Active Directory records indicate that $UserUsername is not able to change their password.</p<p>To prevent this message from occcuring in the future, a Domain Admin needs to login and uncheck the option.</p><p><strong>Admin Insight: </strong>$UserDn</p>"

        $EmailBodyHtml = $ActionedUser | ConvertTo-Html -Body $Body | Out-String
        $EmailSentTo += (Send-Email "$AdminEmailAddress" "$EmailSubject" "$EmailBodyHtml")
        continue
    } elseif (($UserPasswordLastSetOn -eq $false) -or ($null -eq $UserPasswordLastSetOn)) {
        # Users who have never set their AD password. Might be a new employee or someone who rarelt uses their network password 
        $ActionedUser | Add-Member NoteProperty "Status" -Value "-network password not set-" -Force
        #$ActionedUsers += $ActionedUser

        #User will get a different email and the rest of the foreach will be skipped
        Write-Log "WARN" "$UserUsername has never set their Active Directory password."
        continue
    }

    if ( ($UserEmailAddress -eq $False) -or ($Null -eq $UserEmailAddress) -or (!(ValidEmail $UserEmailAddress ))) {
        # Users who don't have an email address specified or if the eamil address doesn't pass verification (i.e. weird characters)
        $ActionedUser | Add-Member NoteProperty "Status" -Value "-invalid email specified-" -Force
        $ActionedUsers += $ActionedUser

        Write-Log "WARN" "$UserUsername has an invalid email address. $AdminEmailaddress will be notificed."
        
        $EmailSubject = "[ADMIN ACTION REQUIRED] $UserUsername does not have an email address specified"
        $Body = "<p>Attention:</p><p>Active Directory records indicate that $UserUsername does not have an email address specified. An e-mail address is required so they can be notified when their password exppires.</p<p>To prevent this message from occcuring in the future, a Domain Admin needs to login and specfiy one.</p>"
        $EmailBodyHtml = $ActionedUser | ConvertTo-Html -Body $Body | Out-String
        $EmailSentTo += (Send-Email "$AdminEmailAddress" "$EmailSubject" "$EmailBodyHtml")
        # No continue because we want to see if the user's password is expired as well
    }
    
    $UserDaysLeft = (New-TimeSpan -Start $Today -End $UserPasswordExpiresOn).Days

    if (($UserDaysLeft -lt 0)) {
        $UserDaysInactive = [Math]::Abs($UserDaysLeft)
        if ($UserDaysInactive -ge $MaxPasswordAge) {
            $ActionedUser | Add-Member NoteProperty "Status" -Value "-possible dormant account-" -Force
        } else {
            $ActionedUser | Add-Member NoteProperty "Status" -Value "-inactive for $UserDaysInactive days-" -Force
        }
        #$ActionedUsers += $ActionedUser
        $UserPasswordLastSetOnPst = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId( ($UserPasswordLastSetOn),'Pacific Standard Time')).ToString('g')
        #Write-Log "WARN" "$UserUsername might have an incative account. Password was last set $UserPasswordLastSetOnPst (Pacific Time) which is more than $MaxPasswordAge days ago!"
    } elseif ( ($UserDaysLeft -gt 0 ) -and ($UserDaysLeft -le $MaximumDays) ) {
        $ActionedUser | Add-Member NoteProperty "Status" -Value "-expires in $UserDaysLeft days-" -Force
        $ActionedUsers += $ActionedUser
        Write-Log "INFO" "$UserUsername password expires in $UserDaysLeft days."
    } elseif (($UserDaysLeft -eq 0)) {
        $ActionedUser | Add-Member NoteProperty "Status" -Value "-expires today-" -Force
        $ActionedUsers += $ActionedUser
        Write-Log "INFO" "$UserUsername password expires in $UserDaysLeft days."
    }
    
    $Body = (
        '<p></p><p>Hi ' + ($UserFirstName) + ',</p><p>Our records indicate that your Active Directory password ' + $(if ($UserDaysLeft -gt 0) { 'will expire in ' + ($UserDaysLeft) + ' days.'  } else { '<strong><em>has expired</em></strong>.' })+ '</p>' +
        '<p>Please visit the [IT Intranet] for more information regarding the [password polic] and instructions on [password change guide]' + 
        '<p>If you have any questions, you can [helpdesk ticket create link[] or email [helpdesk email address]'
    )
    $PostContent = '<p><em>This message was sent on ' + ($ScriptStartTime) + ' by the [Company] IT team as a friendly reminder.</em</p>'
    if ( $NotifyDays.Contains($UserDaysLeft) ) {
       
        # This user will be notified to change their password
        Write-Log "INFO" "$UserUsername password will expire in $UserDaysLeft days and will be notified."
        
        if ($EmailsEnabled -eq $true) {
            # Removing the cannotchangepassword property because the user will get a copy of the $User object later and it's not very user friendly
            $User.PSObject.properties.remove('CannotChangePassword')
    
            # Removing the DN property because the user will get a copy of the $User object later and it's not very user friendly
            $User.PSObject.properties.remove('dn')
             
            # Email notification to user
            if ($UserDaysLeft -eq 1) {
                $EmailSubject = "[IMMEDIATE ACTION REQUIRED] "
            } else {
                $EmailSubject = "[ACTION REQUIRED] "
            }
            $EmailSubject = $EmailSubject+"Network password expiry notification for $UserUsername"
            $EmailBodyHtml = $ActionedUser | ConvertTo-Html -Body $Body -PostContent $PostContent | Out-String
            $EmailSentTo += (Send-Email "$UserEmailAddress" "$EmailSubject" "$EmailBodyHtml")
        }
    } elseif ($UserDaysLeft -eq 0) {
            # This user's password has expired today 
            Write-Log "WARN" "$UserUsername password expires today. They should change it immediately."
           
            # Email notification to user
            $EmailSubject = "[IMMEDIATE ACTION REQUIRED] Network password expired notification for $UserUsername"
            $EmailBodyHtml = $ActionedUser | ConvertTo-Html -Body $Body -PostContent $PostContent | Out-String
            $EmailSentTo += (Send-Email "$UserEmailAddress" "$EmailSubject" "$EmailBodyHtml")
    } else {
            if ($UserDaysLeft -lt 0) {
                Write-Log "WARN" "$UserUsername password expired for $UserDaysInactive days."    
            } else {
                Write-Log "DEBUG" "$UserUsername password expires in $UserDaysLeft days."    
            }
    }

    if ($EmailSentTo.length -gt 0) {
        $ActionedUser | Add-Member NoteProperty "Notified" -Value $EmailSentTo -Force
        $ActionedUsers += $ActionedUser
    }
}

$ScriptEndTime = (Get-Date -f s)
if ($logging -eq $true) {
    if ($EmailLogEnabled -eq $true) {
        Write-Log "LOG" "=================== FINISH ($ScriptEndTime) ==================="
        # Email notification to admin
        $EmailSubject = "Password Expiry Summmary"
        $Body = "<h2>Password Expiry Summary</h2><p>This is a complete list of any issues during the indicated run:</p>"
        $PostContent = "<p><em>This message was sent on ($ScriptStartTime) by the Password Expiry script on  $env:COMPUTERNAME.</em</p>"
        $EmailBodyHtml = $ActionedUsers | Sort-Object 'Password Status','First Name' | ConvertTo-Html -Body $Body -PostContent $PostContent | Out-String
        Send-MailMessage -SmtpServer $SmtpServer -Port $SmtpPort -UseSsl -From $FromEmailAddressObject -To $AdminEmailAddress -Credential $CredentialObject -Subject $EmailSubject -BodyAsHtml -Body $EmailBodyHtml -Attachments $LogFile
    } else {
        Write-Log "LOG" "=================== FINISH ($ScriptEndTime) ==================="
    }


}
##### Password Exiry Script - End                                    #####
Soon

Tags

 

activedirectory
email
notification
password
powershell
useraccount
window server
windows
windowsserver

Comments Beta

 

Be the first to comment on this post!

Your personal data is secure.

Learn about how your information is collected, used, and securely stored in the Privacy Policy.