Powershell : Password expiry notification + HTML messages

If you have a large amount of employees in your domain there is a fair chance you would like them to be notified about expiring passwords and over the years windows gives you friendly reminders about when your password is due to be expiring - this is what it looks like, first, we have the windows 10 style notification:

Then we have the windows 11 style notification (yes done a different times)


Change Password : Physical Hardware

If you are using a physical device then that’s fantastic, you can press CTRL + ALT + DEL then you can choose the change password option once you entered your old and confirmed your new password, your password has been changed, your all set!

Change Password : Thin Clients or VDI Issues

If however, you are using a VDI session based platform when you utilise thin clients then this particular key combination if they’re running Windows will give you the thin client options not the VDI session client security and this could be confusing for your users, if your thin clients run Linux then then CTRL+ALT+DEL may no do anything useful (depending on configuration>

Change Password : Thin Clients or VDI with Powershell

If you wish to accomplish this thin client environment, you can simply create a desktop shortcut or a start menu item that points to this command:

(New-Object -COM Shell.Application).WindowsSecurity( )


When this icon is run, it will invoke the windows security dialogue where you can link to click change password and you all set!

Password expiry notification does not appear?

If you find the password expiry notification does not seem to appear then your password will simply expire and you won’t know it’s expired until you try to login and it fails, many VDI (specially, Citrix) will not allow you to login with The options set on your account of  “User must change password on login”

This ironically is the default option that usually gets set when you change your password through the active directory uses and computers, This is a good security practice because the password someone else will not be personal and unique yourself so there’s a high chance you will forget it or write it down somewhere - both of these are bad ideas.

Security failure : UAC Disabled

The other more sinister reason you don’t get this notification anymore is down to the fact you’ve disabled User Account Control (UAC) - And this is usually from a security point of view a very, very bad idea, and you should not really be disabling this below the recommended settings, this feature was introduced in Windows Vista and it was added Microsoft for security and it should really be on the recommended setting:


It is not recommended to change it to something like this to fix a problem, notice that actually says not recommended because it’s a very foolish move to make:


Note : If you have actually set UAC to the not recommended mode to fix a problem then you haven’t done a very thorough job of fixing the problem because The lack of this security will absolutely backfire on you at some point.

What UAC Is essentially all about is running processes with standard user privileges and not administrative privileges unless their specifically requested, So if you run explorer in the normal fashion, you will not have access to other users folders and data because for that you would need to be in administrator.

You would need to click on the process and choose the option “run as administrator” as shown below: 


When you click this one of two things will happen.

1. If you are a standard user, depending on your set up, you will then be prompted for your administrative credentials:


2. If you are already in administrator, then you will be prompted if you would like to allow the program to run in this mode, however, however you should not be a local administrator on anything unless you absolutely have to be for troubleshooting or diagnostic purposes


UAC Disabled : No Password notification

Yes you will not be notified 🔔 of your password expiring and this is for a number of reasons, but here are a couple of them:

When User Account Control (UAC) is disabled in Windows, it can impact how certain notifications and alerts are displayed, including password expiry notifications due to:

1. Notification Mechanism: Password expiry notifications in Windows are often displayed through system notifications that require elevated privileges to be shown properly. With UAC disabled, the mechanism that elevates these notifications might not function correctly, preventing the notification from appearing.

2. Permission and Security Context: UAC provides a way to separate standard user privileges from administrative privileges. When UAC is disabled, the security context under which notifications and system alerts are generated may change. This change can interfere with how and whether these notifications are presented to the user.

3. System Integrity and Security Features: UAC is designed to improve security by limiting the potential damage of malicious software. Disabling UAC might inadvertently disable or interfere with other integrated security features that manage password policies and notifications.

🚨 UAC Summary : Keep in Enabled 

UAC plays a significant role in managing the security context and permissions necessary for displaying system notifications, including those related to password expiry. Disabling UAC can disrupt these mechanisms, leading to missed notifications. To ensure you receive password expiry notifications, it’s advisable to keep UAC enabled.

Do not turn this off to fix problems with applications or services.

🔔 No Password Notification

So, For one reason or another, the notifications done so the way to fix this is to send notification to people in their inbox Which means they will be notified from day 14 (which is customizable) write down to a couple of days after their password has expired…..

You may be asking how is that possible, How can I use our access mailbox when they can’t login?

They won’t be able to access Outlook like they usually would because their password is expired, but if you have Outlook on their iOS or Android device - these applications use tokenized authentication, which means when your password has expired it won’t necessarily immediately expire your token unless your current authentication request has been revoked by your security administrator.

This means you can still receive the emails saying your password has expired for up to about two days after it expired depending on your conditional access policy, if you don’t have any limits defined you may get more time.

Scripting : notify users via email

Note : There are many scripts that do this that you can download after Microsoft Gallery or people have customized their own scripts, however, when I looked around at the other Scripps you got a very cold and clinical unformatted email, it is essentially basic static text and if you were lucky, you received a hyperlink.

This script gives you a more pleasing UI, that looks more professional, and rather than using images that could possibly get blocked, as it uses Emoji/Unicode (like) characters, like this:


HTML Message : message.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 0;
            padding: 0;
            background-color: #f4f4f4;
        }
        .email-container {
            max-width: 600px;
            margin: 0 auto;
            background-color: #ffffff;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        .header {
            text-align: center;
            padding: 20px 0;
        }
        .header img {
            max-width: 100%;
            height: auto;
            border-radius: 8px;
        }
        .content {
            padding: 20px;
        }
        .content h1 {
            color: #333333;
            font-size: 24px;
            margin-bottom: 20px;
        }
        .content p {
            color: #666666;
            font-size: 16px;
            line-height: 1.5;
        }
        .footer {
            text-align: center;
            padding: 20px 0;
            color: #999999;
            font-size: 14px;
        }
    </style>
</head>
<body>
    <div class="email-container">
        <div class="content">
 👋<br>
    
<p>Dear {UserName},</p>
The password on your account {MessageDays}.</p>
<p>There’s no need to growl and get all hot and flustered. We’ve made it quick and easy to change your password yourself from <a href="<password-reset-url>" target=_blank>here</a>.</p>
<p>If you have a company phone use the BearCave appplication and choose the "Change my passord" option</p>
<p>Remember the corporate password policy when updating your password:</p>
<ul>
    <li>You are not able to use the previous 20 passwords</li>
    <li>Your password needs to use special characters and numbers</li>
    <li>Your password needs to be at least 22 characters</li>
</ul>
<p>Thank you,<br>
Chief Bear Officer<br>
</p>

        </div>
        <div class="footer">
This is an automated email please do not respond to this email.
            </div>

Script : HTML-PasswordNotifySingleUser.ps1

This is the script that will send to a single user to ensure you are happy with the results, this code is yellow not amber, the amber code is for the whole domain

$targetUserEmail = "<upn_of_single_user>"  # specify the target user's email address
$smtpServer = "<smtp_server>"
$expireindays = 7  # number of days of soon-to-expire passwords. i.e. notify for expiring in X days (and every day until $negativedays)
$negativedays = -2  # negative number of days (days already-expired). i.e. notify for expired X days ago
$from = "Password Expiry Reminder <password.expiry@bear.local>"
$logging = $true  # Set to $false to Disable Logging
$logFile = "password.csv"  # 
$testing = $false  # Set to $false to Email Users
$adminEmailAddr = "password.manager@bear.local
$sampleEmails = "1"  # number of sample email to send to adminEmailAddr when testing

# System Settings
$textEncoding = [System.Text.Encoding]::UTF8
$date = Get-Date -format yyyy-MM-dd

$starttime = Get-Date  # need time also; don't use date from above

Write-Host "Processing user: $targetUserEmail for Password-Expiration-Notifications"

# Create CSV Log
if ($logging -eq $true) {
    # Always purge old CSV file
    Out-File $logfile
    Add-Content $logfile "`"Date`",`"SAMAccountName`",`"DisplayName`",`"Created`",`"PasswordSet`",`"DaystoExpire`",`"ExpiresOn`",`"EmailAddress`",`"Notified`""
}

# Get the Target User from AD
Import-Module ActiveDirectory
$user = Get-ADUser -Filter {mail -eq $targetUserEmail} -Properties sAMAccountName, displayName, PasswordNeverExpires, PasswordExpired, PasswordLastSet, EmailAddress, lastLogon, whenCreated

if ($null -eq $user) {
    Write-Host "User with email $targetUserEmail not found."
    exit
}

$DefaultmaxPasswordAge = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge

$dName = $user.displayName
$sName = $user.sAMAccountName
$emailaddress = $user.emailaddress
$whencreated = $user.whencreated
$passwordSetDate = $user.PasswordLastSet
$sent = ""  # Reset Sent Flag

$PasswordPol = (Get-ADUserResultantPasswordPolicy $user)
# Check for Fine Grained Password
if (($PasswordPol) -ne $null) {
    $maxPasswordAge = ($PasswordPol).MaxPasswordAge
} else {
    # No FGPP set to Domain Default
    $maxPasswordAge = $DefaultmaxPasswordAge
}

# If maxPasswordAge=0 then same as passwordNeverExpires, but PasswordCannotExpire bit is not set
if ($maxPasswordAge -eq 0) {
    Write-Host "$sName MaxPasswordAge = $maxPasswordAge (i.e. PasswordNeverExpires) but bit not set."
}

$expiresOn = $passwordsetdate + $maxPasswordAge
$today = (Get-Date)

if (($user.passwordexpired -eq $false) -and ($maxPasswordAge -ne 0)) {   # not Expired and not PasswordNeverExpires
    $daystoexpire = (New-TimeSpan -Start $today -End $expiresOn).Days
} elseif (($user.passwordexpired -eq $true) -and ($passwordSetDate -ne $null) -and ($maxPasswordAge -ne 0)) {   # if expired and passwordSetDate exists and not PasswordNeverExpires
    # i.e. already expired
    $daystoexpire = -((New-TimeSpan -Start $expiresOn -End $today).Days)
} else {
    # i.e. (passwordSetDate = never) OR (maxPasswordAge = 0)
    $daystoexpire = "NA"
}

# Set verbiage based on Number of Days to Expiry.
Switch ($daystoexpire) {
    {$_ -ge $negativedays -and $_ -le "-1"} {$messageDays = "has expired"}
    "0" {$messageDays = "will expire today"}
    "1" {$messageDays = "will expire in 1 day"}
    default {$messageDays = "will expire in " + "$daystoexpire" + " days"}
}

# Email Subject Set Here
$subject = "Your password $messageDays"

# Read HTML Body from the file
$bodyTemplatePath = "message.html"
if (Test-Path $bodyTemplatePath) {
    $body = Get-Content -Path $bodyTemplatePath -Raw
} else {
    Write-Host "HTML template file not found: $bodyTemplatePath"
    $body = "<p>Template file not found. This is an automated message please do not reply to this e-mail</p>"
}

# Replace placeholders in the template with dynamic content
$body = $body -replace "\{MessageDays\}", $messageDays
$body = $body -replace "\{UserName\}", $dName

# If testing-enabled and send-samples, then set recipient to adminEmailAddr else user's EmailAddress
if ($testing -eq $true) {
    $recipient = $adminEmailAddr
} else {
    $recipient = $emailaddress
}

# If in trigger range, send email
if (($daystoexpire -ge $negativedays) -and ($daystoexpire -lt $expireindays) -and ($daystoexpire -ne "NA")) {
    # Send Email Message
    if (($emailaddress) -ne $null) {
        try {
            Send-MailMessage -SmtpServer $smtpServer -From $from -To $recipient -Subject $subject -Body $body -BodyAsHtml -Priority High -Encoding $textEncoding -ErrorAction Stop -ErrorVariable err
            Write-Host "Sent email for $sName to $recipient"
        } catch {
            Write-Host "Error: Could not send email to $recipient via $smtpServer"
        }
    } else {
        Write-Host "$dName ($sName) has no email address."
    }

    # If Logging is Enabled Log Details
    if ($logging -eq $true) {
        Add-Content $logfile "`"$date`",`"$sName`",`"$dName`",`"$whencreated`",`"$passwordSetDate`",`"$daystoExpire`",`"$expireson`",`"$emailaddress`",`"Sent`""
    }
}

$endtime = Get-Date
$totaltime = ($endtime - $starttime).TotalSeconds
$minutes = "{0:N0}" -f ($totaltime / 60)
$seconds = "{0:N0}" -f ($totaltime % 60)

Write-Host "Processing completed in $minutes minutes $seconds seconds."

if ($logging -eq $true) {
    # sort the CSV file
    Rename-Item $logfile "$logfile.old"
    Import-Csv "$logfile.old" | Sort-Object ExpiresOn | Export-Csv $logfile -NoTypeInformation
    Remove-Item "$logFile.old"
    Write-Host "CSV File created at ${logfile}."

    # email the CSV and stats to admin(s) 
    if ($testing -eq $true) {
        $body = "<b><i>Reporting Mode ONLY.</i></b><br>"
    } else {
        $body = ""
    }

    $body += "CSV Attached for"
    $subject = "Password Expiry Notification Report"
    try {
        Send-MailMessage -SmtpServer $smtpServer -From $from -To $adminEmailAddr -Subject $subject -Body $body -Attachments $logfile -BodyAsHtml -Priority High -Encoding $textEncoding -ErrorAction Stop -ErrorVariable err
        Write-Host "Sent admin email with CSV attachment."
    } catch {
        Write-Host "Error: Could not send admin email via $smtpServer"
    }
}

Script : HTML-PasswordNotify.ps1

This will email to all the users in your ADDS with the HTML formatted e-mail it also for reference excludes a domain I do not want to send to which is badbear.local - the full domain code is orange not yellow.

$SearchBase="<BASE_DN_PATH>"
$smtpServer="<SMTP_SERVER>"
$expireindays = 7 #number of days of soon-to-expire passwords. i.e. notify for expiring in X days (and every day until $negativedays)
$negativedays = -2 #negative number of days (days already-expired). i.e. notify for expired X days ago
$from = "Password Expiry Reminder <password.expiry@bear.local>"
$logging = $true # Set to $false to Disable Logging
$logNonExpiring = $false
$logFile = "password.csv" # ie. c:\mylog.csv
$testing = $false # Set to $false to Email Users
$adminEmailAddr = "password.manager:bear.local" 
$sampleEmails = "1" #number of sample email to send to adminEmailAddr when testing ; in the form $sampleEmails="ALL" or $sampleEmails=[0..X] e.g. $sampleEmails=5 or $sampleEmails=3 or $sampleEmails="all" are all valid.
#
###################################################################################################################

# System Settings
$textEncoding = [System.Text.Encoding]::UTF8
$date = Get-Date -format yyyy-MM-dd

$starttime=Get-Date

Write-Host "Processing `"$SearchBase`" for Password-Expiration-Notifications"

#set max sampleEmails to send to $adminEmailAddr
if ( $sampleEmails -isNot [int]) {
    if ( $sampleEmails.ToLower() -eq "all") {
    $sampleEmails=$users.Count
    } #else use the value given
}

if (($testing -eq $true) -and ($sampleEmails -ge 0)) {
    Write-Host "Testing only; $sampleEmails email samples will be sent to $adminEmailAddr"
} elseif (($testing -eq $true) -and ($sampleEmails -eq 0)) {
    Write-Host "Testing only; emails will NOT be sent"
}

# Create CSV Log
if ($logging -eq $true) {
    #Always purge old CSV file
    Out-File $logfile
    Add-Content $logfile "`"Date`",`"SAMAccountName`",`"DisplayName`",`"Created`",`"PasswordSet`",`"DaystoExpire`",`"ExpiresOn`",`"EmailAddress`",`"Notified`""
}

# Get Users From AD who are Enabled, Passwords Expire
Import-Module ActiveDirectory
$users = get-aduser -SearchBase $SearchBase -Filter {(enabled -eq $true) -and (passwordNeverExpires -eq $false) -and (mail -ne "badbear.local")} -properties sAMAccountName, displayName, PasswordNeverExpires, PasswordExpired, PasswordLastSet, EmailAddress, lastLogon, whenCreated
$DefaultmaxPasswordAge = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge

$countprocessed=${users}.Count
$samplesSent=0
$countsent=0
$countnotsent=0
$countfailed=0

# Process Each User for Password Expiry
foreach ($user in $users) {
    $dName = $user.displayName
    $sName = $user.sAMAccountName
    $emailaddress = $user.emailaddress
    $whencreated = $user.whencreated
    $passwordSetDate = $user.PasswordLastSet
    $sent = "" # Reset Sent Flag

    $PasswordPol = (Get-AduserResultantPasswordPolicy $user)
    # Check for Fine Grained Password
    if (($PasswordPol) -ne $null) {
        $maxPasswordAge = ($PasswordPol).MaxPasswordAge
    } else {
        # No FGPP set to Domain Default
        $maxPasswordAge = $DefaultmaxPasswordAge
    }

    #If maxPasswordAge=0 then same as passwordNeverExpires, but PasswordCannotExpire bit is not set
    if ($maxPasswordAge -eq 0) {
        Write-Host "$sName MaxPasswordAge = $maxPasswordAge (i.e. PasswordNeverExpires) but bit not set."
    }

    $expiresOn = $passwordsetdate + $maxPasswordAge
    $today = (get-date)

    if ( ($user.passwordexpired -eq $false) -and ($maxPasswordAge -ne 0) ) {   #not Expired and not PasswordNeverExpires
$daystoexpire = (New-TimeSpan -Start $today -End $expiresOn).Days
    } elseif ( ($user.passwordexpired -eq $true) -and ($passwordSetDate -ne $null) -and ($maxPasswordAge -ne 0) ) {   #if expired and passwordSetDate exists and not PasswordNeverExpires
        # i.e. already expired
    $daystoexpire = -((New-TimeSpan -Start $expiresOn -End $today).Days)
    } else {
        # i.e. (passwordSetDate = never) OR (maxPasswordAge = 0)
    $daystoexpire="NA"
        #continue #"continue" would skip user, but bypass any non-expiry logging
    }

    #Write-Host "$sName DtE: $daystoexpire MPA: $maxPasswordAge" #debug

    # Set verbiage based on Number of Days to Expiry.
    Switch ($daystoexpire) {
        {$_ -ge $negativedays -and $_ -le "-1"} {$messageDays = "has expired"}
        "0" {$messageDays = "will expire today"}
        "1" {$messageDays = "will expire in 1 day"}
        default {$messageDays = "will expire in " + "$daystoexpire" + " days"}
    }

    # Email Subject Set Here
    $subject="Your password $messageDays"

    # Read HTML Body from the file
    $bodyTemplatePath = "message.html"
    if (Test-Path $bodyTemplatePath) {
        $body = Get-Content -Path $bodyTemplatePath -Raw
    } else {
        Write-Host "HTML template file not found: $bodyTemplatePath"
        $body = "<p>Template file not found. This is an automated message please do not reply to this e-mail</p>"
    }

    # Replace placeholders in the template with dynamic content if needed
    $body = $body -replace "\{MessageDays\}", $messageDays

    # If testing-enabled and send-samples, then set recipient to adminEmailAddr else user's EmailAddress
    if (($testing -eq $true) -and ($samplesSent -lt $sampleEmails)) {
        $recipient = $adminEmailAddr
    } else {
        $recipient = $emailaddress
    }

    #if in trigger range, send email
    if ( ($daystoexpire -ge $negativedays) -and ($daystoexpire -lt $expireindays) -and ($daystoexpire -ne "NA") ) {
        # Send Email Message
        if (($emailaddress) -ne $null) {
            if ( ($testing -eq $false) -or (($testing -eq $true) -and ($samplesSent -lt $sampleEmails)) ) {
                try {
                    Send-Mailmessage -smtpServer $smtpServer -from $from -to $recipient -subject $subject -body $body -bodyasHTML -priority High -Encoding $textEncoding -ErrorAction Stop -ErrorVariable err
                } catch {
                    write-host "Error: Could not send email to $recipient via $smtpServer"
                    $sent = "Send fail"
                    $countfailed++
                } finally {
                    if ($err.Count -eq 0) {
                        write-host "Sent email for $sName to $recipient"
                        $countsent++
                        if ($testing -eq $true) {
                            $samplesSent++
                            $sent = "toAdmin"
                        } else { $sent = "Yes" }
                    }
                }
            } else {
                Write-Host "Testing mode: skipping email to $recipient"
                $sent = "No"
                $countnotsent++
            }
        } else {
            Write-Host "$dName ($sName) has no email address."
            $sent = "No addr"
            $countnotsent++
        }

        # If Logging is Enabled Log Details
        if ($logging -eq $true) {
            Add-Content $logfile "`"$date`",`"$sName`",`"$dName`",`"$whencreated`",`"$passwordSetDate`",`"$daystoExpire`",`"$expireson`",`"$emailaddress`",`"$sent`""
        }
    } else {
        #if ( ($daystoexpire -eq "NA") -and ($maxPasswordAge -eq 0) ) { Write-Host "$sName
}
}
Previous Post Next Post

نموذج الاتصال