One of the most common problems in corporate environments is password expiration. With the PowerShell script I introduce in this article, your users will receive automated emails when their Active Directory account passwords are about to expire.

Mike Kanakos

Mike is a Windows IT pro located in the Research Triangle Park area of North Carolina with 13+ years of experience as an admin and 20 years in the field. He specializes in Active Directory, Azure AD, Group Policy, and automation via PowerShell. You can follow Mike's blog at networkadm.in or on Twitter at @MikeKanakos.

You know the thought process: regular password expiration is supposed to make organizations safer. However, password expiration also generates calls to the helpdesk when users forget to change passwords before the expiration occurs. One easy way to deal with users forgetting to change their passwords is notify them shortly before expiration. Today, I will show you one example of doing this via an automated email reminder that a nightly PowerShell script generates via a scheduled task.

The reminder emails are straightforward to generate once we figure out some parameters to use for finding passwords about to expire. In my example, I will check for the password expiration date of all Active Directory accounts but skip checking any accounts that have non-expiring passwords, null passwords, or disabled ones. The script will then send emails to the users seven days prior to password expiration, followed by three days prior and then finally one day prior to password expiration.

My script consists of four major parts:

  1. Getting the password expiration date for each user,
  2. Calculating the days remaining until password expiration,
  3. Configuring the mail message to send, and
  4. Sending the email message.

The last piece is to set up the script to run regularly. We can do this by setting up a scheduled task to run the script. But I will not be covering the configuration of the scheduled task because it is easy to do and there are a ton of references to follow.

We're going to walk through each part of the script in detail, and then we'll put it all together at the end.

Getting the password expiration date for each user ^

The first step in creating the script is to query all user accounts and expose their password expiration dates:

Let's break this search down into smaller chunks that make it easier to understand:

Here we are searching for the following:

  • Enabled users
  • Users who do not have a password that never expires
  • Users who have a password set (PasswordLastSet -gt 0) because we want to skip users with null passwords

TIP: You could customize this search many ways. Two examples would be to target a specific organizational unit (OU) or maybe a set of accounts that match a name (such as admin accounts).

Then we ask for specific properties to return; we need the EmailAddress for later on. The value msDS-UserPasswordExpiryTimeComputed contains the user's password expiration date. However, the password expiry time is not in a human-readable form, so we have to do a conversion:

This creates a hash table and converts the time to a human-readable format. It saves the value into the variable named PasswordExpiry. Notice at the end I use tolongdatestring. This returns the long format of the date (Sunday, March 25, 2018). I chose this because I thought it would be the most useful to display the day and date for my end users. Also, be aware that I deliberately did not display the actual time of the password expiration, only the day. Dropping the actual time makes it easier to do the comparison for password expiration.

Calculating days remaining until password expiration ^

The date calculation comes from a simple date match. I could have done subtraction of today's date versus some future dates, but I thought a date match would be simpler. To perform the date match we need to calculate what the dates are one, three, and seven days from today. I calculated the dates by adding the number of days (1, 3, 7) to today's date and saved them to three separate variables for later use in date comparisons.

Now that we have the dates, we can compare the password expiry date to the dates in the three variables.

You can see above that I started an IF statement. For this script, I use an IF/ELSE statement because the password expiration date can only match one day at a time. In other words, a password that expires in three days can only match the three-day warning date. Because of this, an IF/ELSE statement makes it easy to compare the dates and keeping moving until it finds a match.

The descriptive way to read the match statement is like this:

  • Check to see if the expiry date is same is the same as the seven-day warn date; if it is then run a command, otherwise…
  • Check whether the password expiry date matches the three-day warn date, then run a command, otherwise…
  • Check whether the password expiry date matches the one-day warn date, then run a command, otherwise…
  • Skip this user and move on to the next one.

Here is the IF/ELSE statement simplified:

Configuring the mail message sent ^

The cmdlet to send mail via PowerShell is aptly named Send-MailMessage, and the syntax is easy to understand.

Look at the cmdlet syntax below, and notice the cmdlet is looking for string values.

The email message I would like to send to my end users is:

I am a bot and performed this action automatically. I am here to inform you that the password for USERNAME will expire in X days on Long Date. Please contact the helpdesk if you need assistance changing your password. DO NOT REPLY TO THIS EMAIL.

The bold text represents variables. We must do some sting manipulation when trying to put variables in the middle of strings; I chose to use joins. I created some email variables to hold the string text for the email. Each string contains text up to the point where we will place a variable.

The join at the end joins the strings and variables together and separates them each with a single space. Pay close attention to the $days variable. It makes the email message customized for each date match (1, 3, and 7 days).

Sending the message ^

Here you can see my mail send syntax; it's nothing complicated. Even though it's a generic mail send message, the join statement earlier customizes the email body with the right number of days until expiration. Again, I created some variables to hold the important mail server information.

Now, let's put it all together into a complete script:

Here's what an actual email from the script looks like after processing:

A password expiration email notification

A password expiration email notification

The next step would be to set up a scheduled task and run the script daily at certain time. You can copy this script and use it as is, or you can use it as a starting block and customize it to your needs. Once configured, it should greatly reduce the number of calls to the helpdesk for assistance with expired passwords.

Join the 4sysops PowerShell group!

4+

Users who have LIKED this post:

  • avatar
  • avatar
Share
19 Comments
  1. Don 1 year ago

    Following the chunks of code is not fun.  If we could download the entire script we could better see the flow.

    2+

    • Don 1 year ago

      Never mind, found it.

      1+

      • Alexander 1 year ago

        Ey where did you find it?

        3+

        • LJ Hatfield 9 months ago

          I also would love to know because the script we have is very robust and provides a lot more than what we need and we are looking to trim it down to something like this.

          0

          • Author
            Mike Kanakos 9 months ago

            LJ,

            The script is embedded in the article. Look for the line that says "Click to Expand code"

            I have an updated version of this script completed. I can post my github repo later this week and then post the link here as well.

            0

  2. Author
    Mike Kanakos 1 year ago

    Don,

    Thanks for the feedback.

    My intention greater than just sharing a script. Instead, I was hoping to show people how to build up a few simple ideas into something a little more complex. Maybe next time I will call out early on that the complete script is at the bottom of the article.

    SIDE NOTE: Some of the article text got copied into the final script block. I have since corrected the error. Previously, the first 8 lines were actual text from the article and were probably confusing.

    Sorry if that caused confusion for any readers.

     

    7+

  3. Paolo Frigo 1 year ago

    Nice article Don! Well done!

    I'm not confused by your article by the way! 🙂

    Can I suggest few way of improving your script?

    I've created similar scripts in almost a compact script format (almost a one-liner) using JSON file for storing my variables, so the script can be signed once after being tested (with pester and simple unit-tests) an can be safely executed knowing that the code wasn't altered after being  deployed, meeting the needs of more restricting execution policies.

    So my first suggestion is decouple where possible settings from the script itself, so is easier to re-use in different environments.

    Second is using require statements, if you're script depends from a module or running as an administrator will it make easier to read and debug.

    #Requires –Modules ActiveDirectory

    Third is stick with the DRY approach (Don't Repeat Yourself). You're design implicitly has some different warning dates (1,3,7), but they are Hard-Coded, if you wan't to remove or add dates you need to alter many lines of code. That is key on preserving your code for a long time without any need to change. Focus some effort on refactoring.

    $WarningDates = 1,3,7

    Use a foreach and a define a function send warning and look how more readable your code will be:

    #check password expiration date and send email on match
    $WarningDays = 1,3,7
    foreach ($user in $users) {
    foreach($WarnDay in $WarningDays){
    if($user.PasswordExpiry -eq (get-date).adddays($WarnDay).ToLongDateString()) {
    $EmailBody=$EmailStub1,$user.name,$EmailStub2,$WarnDay,$EmailStub3,$SevenDayWarnDate,$EmailStub4-join' '
    Send-MailMessage-To $user.EmailAddress-From$MailSender-SmtpServer $SMTPServer-Subject $Subject`
    -Body $EmailBody
    }
    }
    }

    Forth... joinin strings is a good strategy, but not the best in this case because the content of the email is not easy to read.  Why don't you create a simple $EmailBody variable?

    $email_body = "Hi $($user.name), ... some text"

    Or my favourite is create different body templates (so I can eventually choose it according to the user or day)  and replacing a string "-USERNAME-". Similar to this

    $email_body = "Hi -USERNAME-, ... some text".Replace("-USERNAME-", $user.name))

    And you can also concatenate replace methods if needed.

    Fifth if you're going to schedule this task you need to provide exit values for knowing from task scheduler when script is executed correctly.

    Sixth, try to avoid comments. Yes, I've said it... Comments are less useful than you think, personally I try to write code is easy to read for everybody.. even a novice of programming.

    Seven, review your code and try to make it shorter and simpler. Refactoring your own code will make you a better developer.

    Sorry if I wrote too much, I really think that your article is good, I thought that with some small steps can be even simpler and efficient and make it great.

     

    10+

    Users who have LIKED this comment:

    • avatar
    • avatar
    • Author
      Mike Kanakos 1 year ago

      Hi Paolo!

      Thanks for taking the time to give me feedback! I love your ideas.

      I know that I still can improve my scripts. My biggest challenge is not thinking about parameterizing my scripts enough.

      I had wanted to do something with the email body like you outlined but somehow I found myself using joins. I guess I got away from my original idea as I got lost in my code. Writing these articles pushes me and I like that, but I fully expect someone (like yourself) to post a comment for every article that says something like, "Hey, here's what you could have done better".

      Again, thanks for the feedback. I am very happy to hear that the article was easy to follow.

      1+

  4. PowerMe! 1 year ago

    Thanks Mike, Paolo! Very nice ideas. I am going to play with it. It is so cool. I think I can use the same concept to send an email to an admin when a user account is locked, or some network/server issues.

    I agree with Paolo about using arrays and variables (email). I would actually create a function for creating the email  out of strings or events. It can be reused elsewhere.
    One thing that I recently learnt from somewhere is to add some some common functions to a power shell Module and load it to your environment (e.g., ISE). I am going to add an email function that I can use in any of my scripts. Thanks for the tip!
    I really doubt if there is ever a "the script"! Scripts evolve - get refined and diversified as we learn, explore possibilities and face challenges. But it is the idea that rules over and makes you explore.
    Paolo do you have an article on JSON in PowerShell? I'd love to see.

    2+

    Users who have LIKED this comment:

    • avatar
  5. Pat Richard 1 year ago

    Use a here-string to build the email message body. MUCH cleaner. Also, you can do some extra work and find the requirements for password complexity to include that info in the email message ("Your password must be at least 8 characters....").

    1+

  6. Kev 1 year ago

    Great script just what I was looking for, I followed your suggestion and targeted a specific OU like so,

    Get-ADUser -SearchBase OU=IT,OU=Root,OU,DC=ChildDomain,DC=RootDomain,DC=com" -Filter *

    thanks for taking the time to write the article.

    2+

  7. Jeff 1 year ago

    Great I was able to user this to create my own script for specific OUs.

    Question

    Is there a way to output the data that the script creates to a file so you can see data and who the emails were sent to?

    2+

  8. Joseph Petersen 9 months ago

    I think time span could have been used with greater effect rather comparing on long date strings

    Since it would allow you to compare based on days and you could also uses the days number when sending the email

    Plus your script is very verbose, consider using a function to generalise sending the email.

    You can also use multiline string to cleanup the email body

    Example of sending email

    0

    • Author
      Mike Kanakos 9 months ago

      Hi Joseph,Thanks for the feedback. You're right, I could have done some things better for sure. This script was completely re-written by me late in 2018. I need to post a link to the updated script. However, I do appreciate the code you wrote in your reply.

      1+

      • Joseph Petersen 9 months ago

        One thing the script also missed was when someone password was already expired 😅

        1+

  9. Joseph Petersen 9 months ago

    Would mind cleaning up my code snippet? 😅

    it was meant to say the following in the foreach loop in the first snippet

    As for second snippet

    Your welcome to delete this reply 🙂

    1+

    Users who have LIKED this comment:

    • avatar

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