Password expiration is delicate topic in every organization. The PowerShell script I wrote allows you to automatically send users an email a number of days before the password expires.

Justin Shin

I have been a Windows administrator for eight years and currently focus on Group Policy, backup, and IIS/Apache administration.

For most sysadmins, resetting passwords is not a fun part of the job. In most organizations it requires much manual intervention: a frantic phone call or email from a user who cannot login, confirmation of the user’s identity, and a password reset. Not only are such requests time-consuming; frequent resets are also a security risk, as admins who deal with them frequently may slip up and forget to confirm the user’s identity before resetting his or her password.

Windows provides warnings a number of days before passwords expire, yet some users ignore them, others are logged on through the notification period, and still others (often remote users) fail to receive them entirely for a variety of reasons.

Thankfully, we do not have to rely on Windows login notifications to notify users that their passwords are soon to expire. We can actually leverage PowerShell to notify users via email! This makes the warnings harder to ignore and more likely to be delivered, especially when dealing with some remote user setups.

PowerShell password expiration notification ^

PowerPasswordNotify.ps1 is a PowerShell script I wrote to get you started on notifying users of password expiration. Here’s the gist of how it works:

  1. Find the maximum password age for your domain
  2. Search for all users in a container you specify
  3. Find all users who have a password that a) expires and b) will expire within a certain number of days that you specify
  4. Notify those users of their impending password expiration via email

At a minimum, you will need to download the script, configure it, and create a scheduled task to run on a reasonable schedule (for instance, every day). You may want to modify or extend the script to suit your needs, though, and I have provided a bit of background on the more opaque aspects of the script at the end of the article.

Configuring the PowerShell script ^

At the top of the script are the configuration variables and functions that you must modify to make the script work, along with some optional parameters. I have denoted the “required” parameters in red.

  • PPNConfig_DebugLevel: 0 is normal operation (sends email notifications); 1 prints the users who would be notified without actually sending the notification.
  • PPNConfig_NotificationTimeInDays: The number of days before expiration (inclusive) that you wish to notify users
  • PPNConfig_SMTPServerAddress: Your SMTP Server Address
  • PPNConfig_FromAddress: Who the email is from
  • PPNConfig_BodyIsHTML: Whether or not to send the email body as HTML
  • PPNConfig_DirectoryRoot: The LDAP container that you wish to search in; this can be your entire domain (“LDAP://dc=corp,dc=company,dc=com”) or more specific (“LDAP://ou=finance,ou=management,dc=corp,dc=company,dc=com”)
  • PPNConfig_MaxPageSize: The maximum number of results you wish to be returned from the directory search

The functions determine the subject and body of the message. Two body functions are defined: one for plain text and the other for HTML email. I have provided some very bare bones implementations.

  • Configure-Notification-Subject($nName, $nNumDays)
  • Configure-Notification-Body-Plain($nName,$nNumDays)
  • Configure-Notification-Body-Html($nName,$nNumDays)

Email notfication

The default notifications are not terribly creative, so adjust to taste!

Schedule the PowerShell script ^

After you have configured the script and tested it manually in PowerShell, you should create a scheduled task in the Task Scheduler (or Scheduled Tasks for Windows Server 2003/R2) to execute the script on a reasonable interval. Because we are searching through the directory for user’s login details you will need to make sure that the user running it is sufficiently privileged –a domain administrator, for example. Make sure that this interval is shorter than the PPNConfig_NotificationTimeInDays setting, or otherwise some users may not get notifications before their passwords expire!

Schedule the PowerShell script

A scheduled task that executes PowerPasswordNotify.ps1

Many admins like to invoke PowerShell scheduled tasks using a batch file. The scheduler runs the batch file; the batch file starts the script. For instance, suppose you have placed PowerPasswordNotify.ps1 in the folder C:\Tasks\PPN. You might want to create a batch file (in your favorite text editor) named “StartPPN.ps1” in that folder with the text:

Of course, depending on your Powershell version and Execution Policy, your mileage may vary.

Some explanations ^

There are a few places in the script that are not obvious as to how and why they work. I’ve identified the biggest potential sources of confusion – and trouble –to help you debug and extend the functionality.

Get-Domain-MaxPassword-Age

This function returns the maximum password age of the executing computer’s domain. This value is provided as a TimeSpan. You may want to extend it to return the policy of a different domain, which you can do by modifying the directory root of the searcher:

Get-Users-With-Expiring-Passwords

In order for us to search the container for enabled users with expiring passwords, we must be narrow down our filter string to include only:

  1. Users: (objectCategory=Person)(objectClass=User)
  2. who are enabled: (!userAccountControl:1.2.840.113556.1.4.803:=2)
  3. with passwords that expire: (!(userAccountControl:1.2.840.113556.1.4.803:=65536)

If you were to delete (3) from the filter string, for example, you would end up with extraneous results – users whose passwords never expire would be notified that their passwords were about to expire.

Finally, you may want to bring over additional information in your implementation of the email notification. This is very easy to do: just add a NoteProperty for the new field that you wish to capture:

Join the 4sysops PowerShell group!

Your question was not answered? Ask in the forum!

0
Share
22 Comments
  1. Serge 6 years ago

    I'm sorry but as a PowerShell enthusiast, I must admit that the wheel has just been reinvented... This VB-script style is just... not real PowerShell.
    Why don't you use MS ActiveDirectory module?
    System.Net.Mail.MailMessage? There's a Send-MailMessage cmdlet + $PSEmailServer variable since PowerShell version 2.0.
    etc.

    0

  2. Author
    Justin Shin 6 years ago

    @Serge, good points. It's a little obtuse. I was trying to present the script and all of the moving parts in a straightforward manner. I don't think it would be difficult to substitute out certain calls (ie sending mail or scouring AD) with slicker ones, though. Plus, people who are completely unfamiliar with PS2 may not have their profiles setup and will need to do additional research into setting $PSEmailServer, for example.

    Still, you make a good point. Even better would be if you could offer an amended script that is more "true" to the intent and style of PS.

    0

  3. Teodor Olteanu 6 years ago

    Hi Justin, great script! Do you have any idea why I get the following error when I execute the script with the DebugLevel = 1? I do get the list of users but first I get the error 3 times.

    Thank you,

    TEO

    Exception calling "InvokeGet" with "1" argument(s): "Exception from HRESULT: 0x8000500D"
    At C:\Users\olteanut\Desktop\PowerPasswordNotify.ps1:63 char:3
    + $PwdChanged = ([adsi]$_.path).psbase.InvokeGet("PasswordLastChanged")
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodTargetInvocation

    -1+

  4. Patrick 6 years ago

    Here is a simplification of the Get-Users-With-Expiring-Passwords function. You would need to fill in your own $UsersOU string

    Hope this helps.

    function Get-Users-With-Expiring-Passwords
    {
    $Date = Get-Date
    $Users = Get-AdUser -LDAPFilter "(objectClass=User)" -Properties * -SearchBase $UsersOU | ? PasswordLastSet -ne $null

    $UsersToNotify = $Users | ? {($Date - $_.passwordlastset).days -le $PPNConfig_NotificationTimeInDays } | % {
    [PSCustomObject] @{
    Name = $_.Name
    Email = $_.mail
    DaysLeft = ($Date - $_.passwordlastset).days
    }
    }

    Return $UsersToNotify
    }

    0

  5. Patrick 6 years ago

    Also, a simplification of the password age function...

    (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge

    0

  6. Patrick 6 years ago

    Whoops. Found a glaring error in my first post.

    function Get-Users-With-Expiring-Passwords
    {
    $Date = Get-Date
    $MaxPasswordAgeDays = (Get-Domain-MaxPassword-Age).Days

    $Users = Get-AdUser -LDAPFilter "(objectClass=User)" -Properties * -SearchBase $UsersOU | ? { ($_.PasswordLastSet -ne $null) -and ($_.PasswordNeverExpires -ne $true) }

    $UsersToNotify = $Users | % {
    $PasswordAgeDays = ($Date - $_.PasswordLastSet).days

    $DaysTillExpiring = $MaxPasswordAgeDays - $PasswordAgeDays

    if (($PPNConfig_NotificationTimeInDays -ge $DaysTillExpiring) -and ($DaysTillExpiring -ge 0))
    {
    [PSCustomObject] @{
    Name = $_.Name
    Email = $_.mail
    DaysLeft = $DaysTillExpiring
    }
    }
    }
    return $UsersToNotify
    }

    0

  7. Patrick 6 years ago

    $Global:MaxPasswordAgeDays = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge.Days

    Function Get-Users-With-Expiring-Passwords {
    $MaxPasswordAgeDays = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge.Days
    $Users = $Global:Users | ? { ($_.PasswordLastSet -ne $null) -and ($_.PasswordNeverExpires -ne $true) }
    Return $Users | ? { (0..$Global:NotificationTimeInDays).Contains((Get-DaysTillExpiring $_)) }
    }

    Function Get-DaysTillExpiring ($User) { Return ($User | % { ($Global:MaxPasswordAgeDays) - (Get-Date - $_.PasswordLastSet).days }) }

    0

  8. MiniAdmin 6 years ago

    Hey Justin, great script, thank you for sharing.
    @Teodor
    I had the same error like you describe. In my case, the error occurs with users who dont have password set yet (new users, with must Change Password at next logon).
    You need to update the filter with "(!pwdLastSet=0)".
    The full script line becames: $DirectorySearcher.filter = "(&(objectCategory=Person)(objectClass=User)(!userAccountControl:1.2.840.113556.1.4.803:=2)(!(userAccountControl:1.2.840.113556.1.4.803:=65536))(!pwdLastSet=0))"

    0

  9. MiniAdmin 6 years ago

    Unlinke Serge, here's a useful update.
    If some of you use FineGrainedPasswords, here's a way to get the MaxPasswordAge of the resultant password policy of the user.
    Edit Code from line 75:

    $PwdChanged = ([adsi]$_.path).psbase.InvokeGet("PasswordLastChanged")
    #Check for Resultant Password Policy of the user
    $ADURPP = Get-ADUserResultantPasswordPolicy ([adsi]$_.path).SamAccountName.ToString()
    If ($ADURPP -ne $Null)
    {
    #Write-Host "UsersPWMaxAge= " $ADURPP.MaxPasswordAge.Days
    $UsersMaxPasswordAgeDays = $ADURPP.MaxPasswordAge.Days
    $DaysTillExpiring = $UsersMaxPasswordAgeDays - ((Get-Date) - $PwdChanged).Days
    }
    else
    {
    $DaysTillExpiring = $MaxPasswordAgeDays - ((Get-Date) - $PwdChanged).Days
    }

    0

  10. Teodor Olteanu 6 years ago

    Thank you MiniAdmin!

    Do you guys have any advice related to how to create a log of the people the e-mails are being sent?

    0

  11. Jonas Koller 6 years ago

    Hey everyone! first of all, thank you very much for your script. i cant get it to run, thats why i hope someone can help me out here.

    Its a german server and i get the following error if i run the script:

    PS C:\inst\powershell> & '.\Neues Textdokument.ps1'
    Ausnahme beim Aufrufen von "Add" mit 1 Argument(en): "Der Wert darf nicht NULL
    sein.
    Parametername: item"
    Bei C:\inst\powershell\Neues Textdokument.ps1:109 Zeichen:20
    + $NewMail.To.Add <<<< ($nEmail)
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException

    Ausnahme beim Aufrufen von "Send" mit 1 Argument(en): "Es muss ein Empfänger a
    ngegeben werden."
    Bei C:\inst\powershell\Neues Textdokument.ps1:123 Zeichen:21
    + $SmtpClient.Send <<<

    As i am a total newbie in scripting i have not really an idea what it could be. but it seems to me that the script is not able to read out the mail address? Can someone tell me how to modify a user properly so i can test the script?

    Thanks for any help in advance!
    Regards Jonas

    0

  12. Jonas Koller 6 years ago

    Sorry the errors weren't copied properly.
    Here again:

    PS C:\inst\powershell> & '.\Neues Textdokument.ps1'
    Ausnahme beim Aufrufen von "Add" mit 1 Argument(en): "Der Wert darf nicht NULL
    sein.
    Parametername: item"
    Bei C:\inst\powershell\Neues Textdokument.ps1:109 Zeichen:20
    + $NewMail.To.Add <<<< ($nEmail)
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException

    Ausnahme beim Aufrufen von "Send" mit 1 Argument(en): "Es muss ein Empfänger a
    ngegeben werden."
    Bei C:\inst\powershell\Neues Textdokument.ps1:123 Zeichen:21
    + $SmtpClient.Send <<<< ($NewMail)
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : DotNetMethodException

    Thanks!

    0

  13. Naresh 6 years ago

    Hi guys, this is grate script but how do I Insert embedded Picture to the email with logo of company?

    0

  14. Rich 6 years ago

    Thanks for posting this, it works well. I think this will save me a lot of headaches!

    One thing I ran into was a problem using multi-line html for the $bodyHtml variable. Once I enclosed it all in @" "@ it worked fine.

    0

  15. Steve 6 years ago

    How do I make the emails high Priority?

    0

  16. Jos Klerks 6 years ago

    last week the script runs very wel. today when i run it i got an error:
    "
    function Get-Domain-MaxPassword-Age
    {
    $ThisDomain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
    $DirectoryRoot = $ThisDomain.GetDirectoryEntry()

    $DirectorySearcher = [System.DirectoryServices.DirectorySearcher]$DirectoryRoot
    $DirectorySearcher.Filter = "(objectClass=domainDNS)"
    $DirectorySearchResult = $DirectorySearcher.FindOne()

    $MaxPasswordAge = New-Object System.TimeSpan([System.Math]::ABS($DirectorySearchResult.properties["maxpwdage"][0]))

    return $MaxPasswordAge
    }

    function Get-Users-With-Expiring-Passwords
    {
    $UsersToNotify = @()

    $DirectoryRoot = New-Object System.DirectoryServices.DirectoryEntry($PPNConfig_DirectoryRoot)
    $DirectorySearcher = New-Object System.DirectoryServices.DirectorySearcher($DirectoryRoot)
    $DirectorySearcher.filter = "(&(objectCategory=Person)(objectClass=User)(!userAccountControl:1.2.840.113556.1.4.803:=2)(!(userAccountControl:1.2.840.113556.1.4.803:=65536)))"
    $DirectorySearcher.pagesize = $PPNConfig_MaxPageSize

    $MaxPasswordAge = Get-Domain-MaxPassword-Age
    $MaxPasswordAgeDays = $MaxPasswordAge.Days

    $DirectorySearchResult = $DirectorySearcher.FindAll() |
    ForEach-Object -ErrorAction "SilentlyContinue" `
    -Process `
    {
    $PwdChanged = ([adsi]$_.path).psbase.InvokeGet("PasswordLastChanged")

    $DaysTillExpiring = $MaxPasswordAgeDays - ((Get-Date) - $PwdChanged).Days

    if ($DaysTillExpiring -le $PPNConfig_NotificationTimeInDays)
    {
    $UserToAdd = New-Object psobject

    $UserToAdd | Add-Member NoteProperty -Name "Name" -Value ([adsi]$_.path).name[0]
    $UserToAdd | Add-Member NoteProperty -Name "Email" -Value ([adsi]$_.path).mail[0]
    $UserToAdd | Add-Member NoteProperty -Name "DaysLeft" -Value $DaysTillExpiring

    $UsersToNotify += $UserToAdd
    }

    }

    return $UsersToNotify
    }

    function Send-Email-Notification-Of-Expiry($nName, $nEmail, $nDaysLeft)
    {
    $SmtpClient = New-Object System.Net.Mail.SmtpClient($PPNConfig_SMTPServerAddress)

    $NewMail = New-Object System.Net.Mail.MailMessage
    $NewMail.From = $PPNConfig_FromAddress
    $NewMail.To.Add($nEmail)
    $NewMail.Subject = Configure-Notification-Subject $nName $nDaysLeft

    if ($PPNConfig_BodyIsHtml)
    {
    $NewMail.IsBodyHtml = $true
    $NewMail.Body = Configure-Notification-Body-Html $nName $nDaysLeft
    }
    else
    {
    $NewMail.IsBodyHtml = $false
    $NewMail.Body = Configure-Notification-Body-Plain $nName $nDaysLeft
    }

    $SmtpClient.Send($NewMail)
    }

    ########
    # MAIN #
    ########

    $UsersToNotify = Get-Users-With-Expiring-Passwords

    foreach ($User in $UsersToNotify)
    {
    if ($PPNConfig_DebugLevel -gt 0)
    {
    Write-Host $User
    }
    else
    {
    Send-Email-Notification-Of-Expiry $User.Name $User.Email $User.DaysLeft
    }
    }
    "

    the code on the line:
    "
    $PwdChanged = ([adsi]$_.path).psbase.InvokeGet("PasswordLastChanged")
    "

    any solution?

    0

  17. Mark 6 years ago

    Has anyone found that when using this variable:

    $PPNConfig_DirectoryRoot = "ldap://ou=test,dc=example,dc=com"

    it will only use the root? I'm not sure why?

    0

  18. Gorafan 5 years ago

    Hi Mark,
    I believe one extra line need to be introduced in function Get-Users-With-Expiring-Passwords

    $DirectorySearcher.SearchScope = "Subtree"

    will be looking like that:

    $DirectoryRoot = New-Object System.DirectoryServices.DirectoryEntry($PPNConfig_DirectoryRoot)
    $DirectorySearcher = New-Object System.DirectoryServices.DirectorySearcher($DirectoryRoot)
    $DirectorySearcher.filter = “(&(objectCategory=Person)(objectClass=User)(!userAccountControl:1.2.840.113556.1.4.803:=2)(!(userAccountControl:1.2.840.113556.1.4.803:=65536))(!pwdLastSet=0))”
    $DirectorySearcher.pagesize = $PPNConfig_MaxPageSize
    $DirectorySearcher.SearchScope = "Subtree"

    From trialing and erro default scope doesn't look entire tree just OU leven by default, changing scope allows search entire subtree

    BTW many thanks to everyone - great effort to get script going in its current form.
    Regards
    Gorafan

    0

  19. John 5 years ago

    How would i edit the script to send it only to an admin, i am looking to use this for service accounts that we use.

    0

  20. aravind 3 years ago

    79 line getting error , Any one suggest it

    Exception calling "FindAll" with "0" argument(s): "There is no such object on the server.

    "

    At C:\Users\......\Documents\password expired.ps1:79 char:5

    +     $DirectorySearchResult = $DirectorySearcher.FindAll() |

    +     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException

    + FullyQualifiedErrorId : DirectoryServicesCOMException

    0

  21. sunny 3 years ago

    how can you add two search base so that i can look two OU as we have two OU for users.

    0

  22. Trevor H 10 months ago

    Getting an error, the recipient needs to be entered.

    0

Leave a reply

Your email address will not be published. Required fields are marked *

*

© 4sysops 2006 - 2019

CONTACT US

Please ask IT administration questions in the forums. Any other messages are welcome.

Sending

Log in with your credentials

or    

Forgot your details?

Create Account