Executive Summary:

Microsoft Windows' interactive logon password expiration notification is useful, but it works only for users who are logging onto the Windows domain interactively by using a Windows client. To address this problem, the PwdNotify.js script uses the email address specified in Active Directory to email users who have passwords that will expire within a specified number of days. The JScript script also logs its activity to the Windows Application event log.

Nearly every Active Directory (AD) domain has the Maximum password age policy setting defined in the default domain policy (i.e., Computer Configuration\Windows Settings\Security Settings\Account Policies\Password Policy). This policy setting defines the number of days that a password is valid. For example, if the Maximum password age policy setting is 90 days, passwords expire after 90 days.

When a user logs on to the domain, Windows checks whether it needs to notify the user that his or her password is due to expire. The Interactive logon: Prompt user to change password before expiration policy setting (in Computer Configuration\Windows Settings\Security Settings\Local Policies\Security Options) specifies when the OS should start notifying the user. For example, a setting of 14 days means that Windows will start prompting users to change their password 14 days before it expires. In Windows 2000, this policy setting is titled Prompt user to change password before expiration—Microsoft added the words Interactive logon to the title of this policy setting in Windows XP and later because it applies only to interactive domain logons.

Interactive logon password expiration notification is useful, but it works only for users who are logging onto the domain interactively by using a Windows OS client. Depending on your organization and the services you provide, interactive logon notification might not be sufficient. For example, Linux users who connect to your organization's Exchange servers by using IMAP clients won't get these password expiration notifications.

To address this problem, I decided to write a script, PwdNotify.js, that uses the email address specified in AD to email users who have passwords that will expire within a specified number of days. The script also logs its activity to the Application event log.

PwdNotify.js and Its Options
PwdNotify.js uses the following command-line syntax:

\[cscript\] PwdNotify.js
  \[/norecurse\] /days:<em>n
</em>  /smtpserver:<em>name
</em>  \[/smtpserverport:<em>port</em>\]
</em>  \[/subject:<em>subject</em>\]

PwdNotify.js requires the CScript host, so you need to use the cscript keyword at the beginning of the command line if CScript is not your default script host. To set CScript as your default script host (recommended), use the following command:

cscript //h:cscript //nologo //s

The location parameter is optional; it specifies the distinguished name (DN) of a domain or organizational unit (OU). The script will search for accounts starting in the location you specify. If you don't specify a location, the script will search for accounts in the current domain. If the DN contains spaces, enclose it in quotes.

The /norecurse option prevents the script from searching for accounts in containers below the specified location. The /days option specifies the number of days that the script should use to determine who requires password expiration notification. For example, /days:5 means that PwdNotify.js should send email to users whose passwords will expire within five days.

The /smtpserver option specifies the name or IP address of the SMTP server that the script should use to send the email messages. If the SMTP server uses a port number other than 25, you can use the /smtpserverport option to specify the port number. The script doesn't support SMTP authentication.

The /from option specifies the sending email address for the SMTP messages that the script sends, and the /subject option specifies the subject line. If either of these values contains spaces, enclose the value in quotes.

For example, consider the following command line:

PwdNotify.js /days:7
 /subject:"Password expiration notice"

This command will search the current domain for accounts that have passwords that will expire within seven days. Figure 1 shows a sample email message, and Figure 2 shows a sample event log entry.

Scheduling the Script
You can use the Task Scheduler service to run PwdNotify.js on a schedule. To do this, follow these steps:

  1. Obtain PwdNotify.js by clicking Download the Code Here button at the top of this Web page. Copy PwdNotify.js to a directory on the server of your choice.
  2. Log onto the server as a member of the Administrators group, go to the Control Panel, and open Scheduled Tasks.
  3. Double-click Add Scheduled Task to start the Scheduled Task wizard, and click Next on the initial page of the wizard.
  4. On the second page of the wizard, click Browse. Browse to the system32 directory and choose cscript.exe as the program you want to run.
  5. On the third page of the wizard, enter a name for the task and specify how often the system should execute the task, as shown in Figure 3, and click Next.
  6. Choose the time, days, and start date on the next page of the wizard, and click Next.
  7. Enter a suitable username and password for the task, and click Next. (To run the task with the system account, enter the username System with a blank password.)
  8. Click Finish on the final page of the wizard.
  9. Right-click the task in the Scheduled Tasks window and choose Properties.
  10. Add the script's path, filename (including the .js extension), and appropriate command-line parameters to the end of the Run line, and click OK. If the script's path contains spaces, enclose the path and filename in quotes. For example, the Run line might look like
      "C:\Documents and Settings
      Administrator\My Documents Scripts
      PwdNotify.js" /days:7
      /subject:"Password expiration notice"
    (all on one line, of course).

Understanding the Script
The PwdNotify.js script first declares some global variables, adds a property to the Number object, and defines the constructor function for the User object, which the script uses later. After this, it executes the main function as an argument to the WScript.Quit method so that the main function's exit code will be the script's exit code.

The main function. In the main function, the script parses the command line by using the WshNamed object, accessible via the WScript.Arguments object's Named property. The WshNamed object contains a collection of command-line options that starts with a slash (/). After the main function parses the named command-line options, it creates a temporary filename and opens the temporary file as a TextStream object. The script writes its output to the temporary file, and it later reads the contents of the temporary file to create the event log message.

Next, the main function checks whether the command line contains an unnamed argument by accessing the WshUnnamed object (accessible via the WScript.Arguments object's Unnamed property). The first unnamed argument corresponds to the location argument on the command line. If there aren't any unnamed arguments, the main function retrieves the default naming context.

Finding the domain's DN. The main function needs to determine the domain component of the DN if the command line specifies a DN. For example, if the command line specifies the DN OU=Sales,OU=Main,DC=wascorp,DC=net, then the script needs to determine that the domain part of the DN is DC=wascorp,DC=net. The script calls the getDomainDN function to perform this task.

The getDomainDN function uses a DN as a parameter and creates a Pathname object to determine its domain component. Listing 1 shows the getDomainDN function. If the DN doesn't start with the domain component, the function creates a Pathname object and configures the Pathname object with the DN. Then it retrieves the parent portion of the DN with the Pathname object's RemoveLeafElement method and checks whether the remaining portion of the name is the domain component. For example, in the DN (OU=Sales,OU=Main,DC=wascorp,DC=net), the getDomainDN function first retrieves the parent portion of the DN (i.e., OU=Main,DC=wascorp,DC=net). Because this isn't the domain component, the function calls RemoveLeafElement again, which now returns DC=wascorp,DC=net.

Finding the domain's maximum password age. Next, the main function determines the domain's maximum password age. Listing 2 shows the getMaxPwdAge function. First, getMaxPwdAge retrieves the domain's maxPwdAge attribute, which is returned as an object containing a 64-bit integer. This 64-bit value represents the number of 100-nanosecond intervals that a password is valid. The object has two properties, HighPart and LowPart, which represent the high and low order 32-bit portions of the 64-bit value.

Next, the getMaxPwdAge function converts the 64-bit value to a 32-bit value and converts the 32-bit value to a number of days by using the formula

(N / 10,000,000) / 86,400

N is the number of 100-nanosecond intervals retrieved from the domain's maxPwdAge attribute. The first division operation converts the number of 100-nanosecond intervals into seconds, and the second division operation divides the number of seconds by 86,400 to get the number of days. The main function stores the maximum password age, in days, in a global variable so that it only needs to be calculated once.

Determining the notification date. Next, the main function determines the notification date. This is the date calculated by adding the number of days specified by the /days option on the command line to the current date. In other words:

Today + N days
= Notification Date

The main function later compares each account's password expiration date with the notification date. If an account's password expiration date occurs before the notification date, the script emails the user.

Searching for accounts. Next, the script calls the getUsers function to find user accounts in the domain. The getUsers function uses ActiveX Data Objects (ADO) to search AD for accounts. Listing 3 shows the excerpt of the script's getUsers function that performs the search. Callout A shows how the function creates and configures the Connection and Command objects. In callout B, the function sets the Command object's CommandText property to the LDAP query string, then calls the Execute method to return a RecordSet object containing the search results. Table 1 shows a breakdown of the LDAP query string. The getUsers function iterates the RecordSet and builds an array of User objects. (The User object is defined at the top of the script.) The function determines when each account's password expires by calling the getPwdExpires function for each account.

It's important to understand that the LDAP query finds only accounts that have a mail attribute. If you're surprised by the script's search results, make sure that the accounts in question have a mail attribute and that the password on the accounts hasn't already expired.

The getPwdExpires function. As the getUsers function iterates the RecordSet object, it retrieves the date each account's password was last changed and adds the domain's maximum password age to get the date the account's password expires. In other words:

 + MaximumPasswordAge
 = PasswordExpirationDate

Listing 4 shows the getPwdExpires function. It first retrieves a reference to the AD User object. It then attempts to create a new Date object containing the PasswordLastChanged value. Next, it adds the domain's maximum password age (a number of days) to this date. If the password has not yet expired, the getPwdExpires function returns the date the password expires.

The getPwdExpires function uses a try ... catch block in case the PasswordLastChanged attribute doesn't exist (meaning the account's password has never been set). If the PasswordLastChanged attribute doesn't exist, or if the password has already expired, the getPwdExpires function returns a "zero" date (i.e., 1 Jan 1970 00:00:00 Coordinated Universal Time—UTC). The result is that the script uses the getPwdExpires function to avoid sending email to users whose passwords have already expired.

Emailing the users. The main function uses Collaboration Data Objects (CDO) to send email. Listing 5 shows the code from the main function that creates and configures the SMTP message. After creating and configuring the CDO object to send email, the function iterates the array of User objects returned from the getUsers function. If the pwdExpires property for each User object is a date sooner than the notification date, the script configures the CDO message object's To and TextBody properties and emails the user.

Finishing up. After the main function has finished iterating the array of User objects, it closes the TextStream object and reopens it. Next, the function reads the logged data as a string using the TextStream object's ReadAll method. It then closes and deletes the file, and calls the WshShell object's LogEvent method to write the information to the Application log.

PwdNotify.js provides a flexible, convenient, and simple way to email users about when their passwords will expire, even if they don't log on to the domain interactively. Schedule it to run on a daily basis to keep your users informed.