How Long Until My Password Expires?

 

Greg Stemp, Dean Tsaltas, and Bob Wells
Microsoft Corporation

Ethan Wilansky
Network Design Group

September 12, 2002

Summary: While work on WMI Scripting Primer: Part 3 continues, the Scripting Guys turn their attention to an important task that can be accomplished with an ADSI script: determining password expiration in an Active Directory network. (22 printed pages)

The question in the title is a simple one—but with a surprisingly involved answer for Active Directory® user accounts. Exploring this question is the topic of this article because, in answering it, you will learn a bit about Active Directory Service Interfaces (ADSI) and how to create scripts to read all kinds of useful information about Active Directory user account objects.

A critically important security policy is requiring that users change their passwords on a regular basis. Inevitably, many users will wait until the last minute to change their passwords, or they will allow their passwords to expire. Depending on your password policy, password expiration might require that support staff reset the user's password. Some types of user accounts, such as service accounts, might be configured so that their password never expires. If your password policy dictates that passwords do expire, and it should, it is helpful to give users some kind of advance notification when their password is about to expire. Support staff will also find it helpful to check service accounts to determine if a particular user account's password is configured to expire.

In creating a script to complete the task of determining password expiration, you must complete the following sub-tasks:

  • Determine if a user account password is set to expire. If the user's Password never expires option is enabled, there's no need to calculate password expiration.
  • Determine when last the user changed their password. If the user's Password never expires option is disabled, as it should be, the next task is to determine when the user last changed their password.
  • Determining what the maximum password age is in the domain. Now that you know that a user account password is set to expire and when last the user changed their password, the next step is to determine the length of time a user is allowed to use their password. This value is dictated by domain policy, so you must read this value from the user's domain. One small caveat here is if the maximum password age in the domain is set to 0, passwords in the domain do not expire. The script must account for this exception.
  • Determine the current date. Knowing the current date, the date when the password was last changed, and the maximum password age in the domain allows the script to calculate how many days remain before a password must be changed.

Using ADSI to Read Password Attributes

Before exploring scripts to complete each sub-task listed in the introduction, you should understand what ADSI is and two fundamental components of ADSI: ADSI providers and ADSI interfaces. ADSI is a set of system components (implemented in a handful of DLLs) that you can use to create scripts to manage directory services, such as Active Directory, Microsoft® Windows® NT/Windows 2000 Security Accounts Manager (SAM), the Microsoft® Internet Information Services (IIS) metabase, Novell® NetWare® Directory Service (NDS), and Novell NetWare 3.x Bindery. Providers are the mechanism your ADSI scripts use to connect to and communicate with different directory types. Each provider supports a variety of ADSI interfaces. ADSI interfaces provide the methods and properties you use to create, read, modify, and delete objects stored in the directory.

Each ADSI provider is designed specifically for a type of directory service. For example, you use the LDAP provider to access LDAP directories, such as Active Directory, the NDS provider to access Novell NetWare Directory Services, and the WinNT provider to access Windows NT domains and Windows NT/Windows 2000/Windows XP local account databases. You can see a list of providers installed on a computer by running the following script:

Set objProvider = GetObject("ADs:")
For Each Provider In objProvider
    WScript.Echo Provider.Name
Next

While each provider is designed for a specific directory type, it is possible to use the WinNT provider to access Active Directory. However, this should be avoided whenever possible. Why, you might ask? Although the WinNT provider can be used to manage a subset of objects and attributes in Active Directory, using the WinNT provider to manage Active Directory is not recommended for the following reasons:

  • Using the WinNT provider limits the Active Directory objects and attributes your scripts can manage. You cannot manage most user attributes using the WinNT provider, for example. Nor can you manage Organizational Units or Universal Groups.
  • Using the WinNT provider to manage Active Directory can have an adverse effect on your script's performance and resiliency.
  • The WinNT provider is less efficient at locating objects in Active Directory.
  • The WinNT provider randomly selects a domain controller to connect to if no domain controller is specified in the connection string. The LDAP provider, on the other hand, tries to connect to a domain controller in the client's local site.
  • Under the covers, the WinNT provider uses the Win32® Net application programming interfaces (APIs)—such as NetUserAdd, NetUserEnum, and NetUserGetInfo—and NetBIOS to communicate with Active Directory. The LDAP provider communicates with Active Directory using LDAP APIs and TCP/IP.

Unfortunately, many scripting books and articles use the WinNT provider in their script examples to access the Active Directory. You should now understand why this should be avoided whenever possible. As you will see in this article, we always use the LDAP provider, even when the WinNT provider provides an easier approach. We will point these out along the way.

Each ADSI provider is identified by a mandatory, case-sensitive prefix, called a moniker. The term moniker comes from the COM world and is just a fancy name for a string that identifies a DLL registered in the HKEY_CLASSES_ROOT registry hive on the local computer. Table 1 lists the monikers for the four ADSI providers.

Table 1. ADSI providers

Name Moniker Libraries Description
LDAP Provider "LDAP:" adsldp.dll
adsldpc.dll
adsmsext.dll
Active Directory provider. Also compatible with LDAP Version 2.0 and Version 3.0 directories.
WinNT Provider "WinNT:" adsnt.dll Windows NT/Windows 2000/Windows XP provider.
NDS Provider "NDS:" adsnds.dll Novell NetWare Directory Service provider.
NWCOMPAT Provider "NWCOMPAT:" adsnw.dll Novell NetWare Bindery provider.

You use the provider's moniker, in combination with the GetObject function in Microsoft Visual Basic®, Scripting Edition (VBScript), to connect to a directory—a process officially known as binding. Suppose, for example, you want to bind to a user object in Active Directory. You use the "LDAP:" prefix followed by the path to the object, as shown here:

Set objUser = GetObject("LDAP://CN=myerken,OU=management,DC=fabrikam,DC=com")

The provider prefix coupled with the object path is called an ADsPath in ADSI terminology. Each provider supports a unique object path syntax. In the case of Active Directory, object paths correspond to the target object's distinguished name (DN).

What does the previous line of VBScript code do? Essentially, it creates a virtual representation of the CN=myerken user in a special cache on the local computer—the computer where the script is running. The variable named objUser points to the virtual Ken object and represents the mechanism you use to read from or make changes to the CN=myerken user object.

After you bind to an object in Active Directory, you use ADSI interfaces to manage the object. What is an interface? An interface is another one of those fancy COM terms; it simply refers to a group of methods and properties inside a DLL. A DLL can contain multiple interfaces and each interface can have multiple methods and properties. You use the methods and properties provided by an interface to manage an object.

In the following example, we are using the Get method, provided by the IADs interface, to retrieve Ken's e-mail address. Providing Ken's e-mail attribute contains a value, we echo it to the screen. Note that if Ken's user account doesn't have an assigned e-mail address, the script generates an error. We will demonstrate how to deal with that situation later in this article. For now, notice how the objUser object reference is used to call the IADs interface's Get method. The Get method accepts one parameter, which is the lDAPDisplayName of the attribute whose value you want to retrieve.

Set objUser = GetObject("LDAP://CN=myerken,OU=management,DC=fabrikam,DC=com")
strMail = objUser.Get("mail")
WScript.Echo strMail

ADSI provides a bunch of interfaces—60 to be exact. However, you only need to be familiar with a few to create most ADSI scripts. The tricky thing to learn about ADSI interfaces is that different ADSI providers support different interfaces. In addition, providers that support the same interfaces don't always support the same set of methods or properties. This will become obvious as we describe the article's scripts. Fortunately, the ADSI documentation on MSDN includes a complete list of the interfaces, methods, and properties each provider supports. See the topic entitled "Provider Support of ADSI Interfaces!ALink ("provider_support_of_adsi_interfaces")" on MSDN.

The interfaces used in this article include:

  • The IADs interface. The IADs interface is a general-purpose interface providing a consistent approach to managing objects and their attributes in the directory. The neat thing about IADs is that once you learn how to read and write attributes for one object type using IADs, you can immediately apply the same approach to other object types. The only thing that changes is the name of the attribute you want to manage.
  • The IADsUser interface. The IADsUser interface provides a generic representation of a user, independent of the underlying directory. Not all attributes of an Active Directory user account object are available using the IADsUser interface.

At this point you might be asking, "Why is it necessary to learn two interfaces when it appears that the IADs core interface is generic and can manage just about anything in the directory?" To this we all say, "Excellent question!" It turns out that some values returned by IADs are not easy to convert into a readable format from VBScript. In addition, some methods provided by the IADsUser interface are not available using the IADs interface. For example, to change a password, you must use the IADsUser interface. As each part of the script is built, we will explain to you exactly why a particular interface was selected to read a value from an attribute.

Where Password Attributes Reside

The scripts in this article read password-related attributes, but whether you are reading or writing values to password attributes, you must know where the attributes reside. Once you know their location, you can more easily determine the appropriate interface and provider to use in order to read their values. Password-related attributes are located in two places: in the domain and in each user account object. Table 2 shows details about the attributes that must be read to determine when a password will expire. The table shows each attribute's name, a description of what the attribute sets, its location in the directory, and the attribute's data type.

Table 2. Attributes used to determine password expiration

Attribute Name Description Location Data Type
maxPwdAge Maximum password age Domain 64-bit large integer
pwdLastSet Password last changed User Account 64-bit large integer
userAccountControl Password never expires User Account 32-bit integer

Attribute names appearing in column one use the lDAPDisplayName. This is the name used by the IADs core interface to reference an attribute. The Data Type column (column 4) appears in Table 2 because knowing the type of data gives you an idea of the easiest way to read the attribute's value.

32-bit integer data types are easy to read and display using IADs and the LDAP provider; 64-bit large integers are easy to read but are not always easy to convert to a usable format, because VBScript doesn't support 64-bit integers. You can create a script to read and display the large integers appearing in Table 2 in a number of ways:

  • Use a method or property from an interface other than IADs (for example, IADsUser) if one exists.
  • Use the WMI Directory Services Provider.
  • Use the IADs core interface, and perform a complex conversion of the high and low integer values contained in a large integer.

This article demonstrates two approaches: How to read attributes stored as large integers using IADsUser and how to convert a large integer retrieved using IADs. The WMI Directory Services Provider isn't used because ADSI is the recommended approach to programmatically managing Active Directory. Although the WMI scripting library provides a helper object (SWbemDateTime) that can be used to convert Active Directory date and time values stored as large integers, learning how to convert the attribute in ADSI is a worthwhile exercise. We should also point out that the WMI SWbemDateTime helper object is only available on Windows XP and Windows Server 2003.

Putting the Pieces Together

Now that you understand the purpose of the ADSI providers and interfaces and you understand a little about how the attributes are stored, the remainder of this article will demonstrate how to create a script that calculates when a user account password will expire.

Here is a recapitulation of the sub-tasks that will be completed with a little more detail about what provider, interface, and property or attribute will be read:

  • Determine if a user account password is set to expire. Use the LDAP provider and the IADs interface to examine the ADS_UF_DONT_EXPIRE_PASSWD flag in the userAccountControl attribute of a user account object.
  • Determine when the user last changed their password. Use the LDAP provider and the IADsUser interface to read the PasswordLastChanged property. This property maps to the pwdLastSet attribute of a user account object.
  • Determine the maximum password age in the domain. Use the LDAP provider and the IADs interface to retrieve the domain's maxPwdAge attribute. Use the ADSI IADsLargeInteger interface to convert maxPwdAge to a usable value.
  • Determine the current date.?Use the VBScript Now function. This function extracts the current date and time from the computer running the script.

The next four sections detail each sub-task in the previous list, and the fifth section shows a script that puts it all together.

Determining if a User account Password Expires

Earlier we said the userAccountControl attribute is a 32-bit integer, and it is. However, userAccountControl isn't used in the traditional way you might use a number. Instead, userAccountControl is a type of integer where each bit in its value represents a unique setting. This type of integer is called a bit field. Because each bit in a bit field represents a different setting, simply examining the integer's value as a whole number is of little use. You must examine the individual bit that corresponds to the setting you're interested in reading.

To help you identify which bit to check, programming libraries like ADSI often include pre-defined constants that map the bits in a bit field to friendly names. The constants serve as bit masks, each of which are used to test whether or not certain bits are set in the bit field.

The set of constants that represent bit masks for properties of the userAccountControl attribute are included in the ADS_USER_FLAG_ENUM enumeration. An enumeration in this context is simply one or more constants grouped together according to their usage. The specific constant that represents a user account's Password never expires option is ADS_UF_DONT_EXPIRE_PASSWD, which is defined as 0x10000, or &h10000 in VBScript.

To determine if a user account expires, you examine the state (1 or 0) of the ADS_UF_DONT_EXPIRE_PASSWD bit in the userAccountControl attribute. To accomplish this task, you must first read the userAccountControl attribute from a user account object. This attribute contains this and other settings. Then, you use the bitwise AND operator along with the setting's bit mask to extract the corresponding bit values from the bit field, as shown in Listing 1.

To carry out this task, the script performs the following steps:

  1. Sets a constant to the value of the ADS_UF_DON'T_EXPIRE_PASSWD flag in the userAccountControl attribute.

  2. Binds to the user account object using the GetObject function and the LDAP provider.

    A specific user named MyerKen in the Management OU of the fabrikam.com domain is hard coded into the script. You need to change the ADsPath passed to GetObject to redirect the script to a user account object in your Active Directory forest. You can also configure the script to automatically determine the user running the script using the IADsADSystemInfo interface.

  3. Retrieves the value of the user's userAccountControl attribute and initializes a variable named intUserAccountControl with this value.

  4. Uses VBScript's bitwise AND operator to determine whether the ADS_UF_DONT_EXPIRE_PASSWD flag is enabled. If it is enabled, displays a message stating that the password doesn't expire and then terminates the script using WScript.Quit. Otherwise, displays a message stating that the password does expire.

Listing 1. Displaying whether a user account password expires

Const ADS_UF_DONT_EXPIRE_PASSWD = &h10000

Set objUser = GetObject("LDAP://CN=myerken,OU=management,DC=fabrikam,DC=com")
intUserAccountControl = objUser.Get("userAccountControl")

If intUserAccountControl And ADS_UF_DONT_EXPIRE_PASSWD Then
    WScript.Echo "The password does not expire."
    WScript.Quit
Else
    WScript.Echo "The password expires."
End If

In Listing 1, it's not necessary to use WScript.Quit, but if the script was larger and tested for other conditions, as the final script in this article does, then you should terminate the script using WScript.Quit so that no other conditions are tested.

Determining When a Password Was Changed

The pwdLastSet attribute (see Table 2) is stored as a 64-bit large integer in each user account object. You can retrieve pwdLastSet in one of two ways using ADSI:

  • Use the IADs interface's Get method.
  • Use the IADsUserPasswordLastChanged property.

Using IADsGet to retrieve the pwdLastSet attribute returns an IADsLargeInteger object representing the number of 100-nanosecond intervals since January 1, 1601. Unfortunately, converting the value to an accurate date using VBScript is difficult at best. Furthermore, you loose some precision during the conversion process since VBScript doesn't support 64-bit integers.

Fortunately, the IADsUser interface provides a property, PasswordLastChanged, which performs the date conversion for you. Therefore, use the PasswordLastChanged property of IADsUser to determine the last time a user last changed their password.

To carry out this task the script performs the following steps:

  1. Binds to the user account object using the GetObject function.

  2. Creates a variable and initialize it to the value returned by the PasswordLastChanged property of the IADsUser interface.

  3. Displays the date and time when the password was last set.

    The dtmValue variable contains both the date and time when a password was last set. Using the DateValue and TimeValue VBScript functions, you can individually extract the two pieces of data stored in the variable.

    While it's not necessary in Listing 2 to extract the date and time values from the variable, there are instances when either the date or the time are needed. In fact, the final script in this article uses the date value only to return the number of days before a password expires.

    The script sample in Listing 2 shows the steps for checking the exact date and time when the MyerKen user account password was last changed. This script uses the PasswordLastChanged property of the IADsUser interface.

Listing 2. Viewing the date and time when a password was last set

Set objUser = GetObject("LDAP://CN=myerken,OU=management,DC=fabrikam,DC=com")
dtmValue = objUser.PasswordLastChanged           ' LINE 2
WScript.Echo "The password was last set on " & _
             DateValue(dtmValue) & " at " & TimeValue(dtmValue)

The value displayed includes both the date and time when the password was last set. If the user account is created using the Active Directory Users and Computers snap-in, and the password has never been set, then the date and time value is equivalent to when the user account was created. However, if you use a script to create a user account and the password is not set in the script, then line 2 of the script appearing in Listing 2 returns the following error message and the script terminates:

C:\Scripts\Listing2.vbs(2, 1) (null): 0x8000500D

This is a common error message in ADSI. It means that an attribute requested in the script cannot be found in the local property cache. The name for this ADSI error code is E_ADS_PROPERTY_NOT_FOUND.

An attribute might not be in the local property cache of the computer running the script because the attribute name is mistyped or a value for the attribute has never been set. If this error message is generated when the code in Listing 2 runs, it's because a value for the pwdLastSet attribute has never been set. Remember, the PasswordLastChanged property of IADsUser maps to the pwdLastSet attribute of a user account object. The code in Listing 3 builds on the code in Listing 2 to handle the E_ADS_PROPERTY_NOT_FOUND error.

To carry out this task, the script performs the following steps:

  1. Uses the VBScript On Error Resume Next statement to catch the E_ADS_PROPERTY_NOT_FOUND error generated when an attribute cannot be found in the local property cache.

  2. Binds to the user account object using the GetObject function.

  3. Creates a variable and initializes it to the value returned by the PasswordLastChanged property of the IADsUser interface.

    The PasswordLastChanged property contains the value of the pwdLastSet attribute of the user account object.

  4. If the pwdLastSet attribute cannot be found in the local property cache, the code on line 6 returns the 0x8000500D error.

    The 0x8000500D error has been set equal to the E_ADS_PROPERTY_NOT_FOUND constant on line 3. Therefore, rather than test for a cryptic error number, the script can use the somewhat more user-friendly E_ADS_PROPERTY_NOT_FOUND constant instead.

  5. Uses the number property of the Err object to test whether the E_ADS_PROPERTY_NOT_FOUND error was returned.

    The Err object is an intrinsic VBScript object. An object is considered intrinsic to the environment if it doesn't need to be explicitly created in the script before it is used.

  6. If the pwdLastSet attribute cannot be found in the local property cache, displays a message stating that the password has never been set and then quits. Otherwise, display the date and time when the password was last set.

    The script sample in Listing 3 shows the steps for checking the exact date and time when the MyerKen user account password was last changed. This script uses the PasswordLastChanged property of the IADsUser interface.

Listing 3. Including error handling to view when or if a password was last set

On Error Resume Next

Const E_ADS_PROPERTY_NOT_FOUND  = &h8000500D     ' LINE 3

Set objUser = GetObject("LDAP://CN=myerken,OU=management,DC=fabrikam,DC=com")
dtmValue = objUser.PasswordLastChanged           ' LINE 6

If Err.Number = E_ADS_PROPERTY_NOT_FOUND Then
    WScript.Echo "The password has never been set."
    WScript.Quit
Else  
    WScript.Echo "The password was last set on " & _
                 DateValue(dtmValue) & " at " & TimeValue(dtmValue)
End If

Determining the Maximum Password Age in the Domain

The maxPwdAge attribute (Table 2) is stored as a 64-bit large integer in the domain object. You can retrieve maxPwdAge one of two ways using ADSI:

  • Use the IADsDomainMaxPasswordAge property.
  • Use the IADs interface's Get method.

The IADsDomain interface is only available to the WinNT provider. Although you can use the WinNT provider, the WinNT provider should not be used to access Active Directory for the reasons we described earlier. As such, we use IADsGet to retrieve the domain's maxPwdAge attribute, even though it's slightly more complicated.

Using IADsGet to retrieve the domain's maxPwdAge attribute returns an IADsLargeInteger object representing the domain's maximum password age measured in 100-nanosecond intervals.

To carry out this task, the script performs the following steps:

  1. Defines two time-related constants.

    ONE_HUNDRED_NANOSECOND will be used to convert the value of maxPwdAge from 100-nanosecond intervals to seconds. SECONDS_IN_DAY will be used to convert seconds to days.

  2. Binds to the domain using the GetObject function in VBScript and the LDAP provider.

    On line 4, the domain's distinguished name, "DC=fabrikam,DC=com", is hard coded into the script. You need to change the ADsPath passed to GetObject to redirect the script to the user's domain. You can also configure the script to automatically determine the default domain using rootDSE in combination with rootDSE's defaultNamingContext.

  3. Uses the IADs interface's Get method to retrieve the value of the domain's maxPwdAge attribute (line 5).

    Notice we use the Set keyword in VBScript to initialize the variable named objMaxPwdAge—the variable used to store the value returned by Get. Why is that?

    When you fetch a 64-bit large integer, ADSI does not return one giant scalar value. Instead, ADSI automatically returns an IADsLargeInteger object. You use the IADsLargeInteger interface's HighPart and LowPart properties to calculate the large integer's value. As you may have guessed, HighPart gets the high order 32 bits, and LowPart gets the low order 32 bits. You use the following formula to convert HighPart and LowPart to the large integer's value.

    LargeIntValue = objLargeInt.HighPart * 2^32 + objLargeInt.LowPart
    
  4. Using IADsLargeIntegerLowPart, checks the low order bits of objMaxPwdAge.

    If the low order bits in objMaxPwdAge are equal to 0, the maximum password age in the domain is set to 0, which means passwords do not expire. A message is echoed indicating passwords do not expire in the domain and the script exits; otherwise, the script continues.

  5. Converts objMaxPwdAge to days.

    This step is divided into the following three sub-steps:

    1. Uses the IADsLargeInteger formula, mentioned above, to calculate the number of 100-nanosecond intervals stored in objMaxPwdAge. VBScript coerces the calculation's result into a double-precision, floating-point number because VBScript doesn't understand 64-bit integers. The VBScript Abs function is used to return the absolute value of the number because maxPwdAge is stored as a negative number. The formula's result is stored in the variable named dblMaxPwdNano.
    2. On Line 13, the 100-nanosecond intervals are converted to seconds by multiplying dblMaxPwdNano by .0000001, or 10^-7. The result is stored in the variable named dblMaxPwdSecs.
    3. Line 14 converts the seconds to days by dividing dblMaxPwdSecs by 86400. The VBScript Int function is used to remove the fractional portion of the floating-point number returned by the calculation. The result is stored in dblMaxPwdDays.
  6. Echoes the domain's maximum password age, and exits.

    The script sample in Listing 4 shows the steps for checking the maximum password age allowed in the domain. This script uses the IADs interface's Get method to retrieve the domain's maxPwdAge attribute.

Listing 4. Viewing the maximum password age in the domain

Const ONE_HUNDRED_NANOSECOND = .000000100   ' .000000100 is equal to 10^-7
Const SECONDS_IN_DAY = 86400

Set objDomain = GetObject("LDAP://DC=fabrikam,DC=com")     ' LINE 4
Set objMaxPwdAge = objDomain.Get("maxPwdAge")              ' LINE 5

If objMaxPwdAge.LowPart = 0 Then
  WScript.Echo "The Maximum Password Age is set to 0 in the " & _
               "domain. Therefore, the password does not expire."
  WScript.Quit
Else
  dblMaxPwdNano = Abs(objMaxPwdAge.HighPart * 2^32 + objMaxPwdAge.LowPart)
  dblMaxPwdSecs = dblMaxPwdNano * ONE_HUNDRED_NANOSECOND   ' LINE 13
  dblMaxPwdDays = Int(dblMaxPwdSecs / SECONDS_IN_DAY)      ' LINE 14
  WScript.Echo "Maximum password age: " & dblMaxPwdDays & " days"
End If

**Note   **If your goal is to write values to domain password attributes, use Group Policy Objects (GPOs) linked to the domain. Domain password attributes apply to all user account objects in the domain.

Determining the Current Time

Determining the current time has nothing to do with ADSI but everything to do with VBScript. VBScript contains the Now function that determines the current time by reading the setting of the local computer's date and time.

To carry out this task, the script performs the following step:

  1. Uses WScript.Echo to display the value contained in the Now function.

Listing 5. Viewing the current date and time

WScript.Echo "The current date and time is: " & Now

Putting It All Together

Now that you have seen the pieces, here's how you can put this together into a script that shows how to determine when or if a password expires.

Figure 1 shows a flow diagram to help you understand the logic in the script.

Figure 1. A flow diagram showing the script logic

Listing 6 contains a script that displays password expiration information for a user. To carry out this task, the script performs the following steps:

  1. Uses the On Error Resume Next statement.

    This statement can be used to catch (or suppress) any run-time error; however, you should use it only if you are testing for, and addressing, errors that might occur when the script runs. In this case, the On Error Resume Next statement is used to catch the ADSI error that is generated if the pwdLastSet attribute cannot be found in the local property cache.

  2. Defines four constants.

    Sets the ADS_UF_DONT_EXPIRE_PASSWD constant equal to the value of the corresponding bit flag in the userAccountControl attribute (used on Line 11).

    Sets the E_ADS_PROPERTY_NOT_FOUND constant equal to the ADSI error code generated if the pwdLastSet attribute cannot be found in the local property cache (used on Line 16).

    Sets the ONE_HUNDRED_NANOSECOND and SECONDS_IN_DAY constants. Both are used later in the script (Lines 37–38) to convert the value of maxPwdAge from 100-nanosecond intervals to days.

  3. Binds to the user account object using the GetObject function and the LDAP provider.

  4. Creates a variable and initializes it to the integer value of the userAccountControl attribute.

  5. Determines whether ADS_UF_DONT_EXPIRE_PASSWD is enabled. If so, then displays a message stating that the user account password does not expire and then quits. Otherwise, continues processing the script.

  6. If the user account password does expire, gets the value of the pwdLastSet attribute from the PasswordLastChanged property of the IADsUser interface.

    The PasswordLastChanged property displays the value of the pwdLastSet attribute in an easily readable and convertible format.

  7. Creates a variable and initializes it to the value returned by the PasswordLastChanged property of the IADsUser interface.

  8. Uses the Number property of the Err object to test whether the E_ADS_PROPERTY_NOT_FOUND error was encountered. If so, displays a message that the password has never been set and then quits. Otherwise, displays the date and time of when the password was last set.

  9. Using the Now function in VBScript, subtracts the current time from the dtmValue variable to determine how much time has elapsed since the password was changed. Creates the intTimeInterval variable and initializes it with this value.

  10. Binds to the domain using the GetObject function and the LDAP provider.

  11. Retrieves the domain's maxPwdAge attribute, which is returned as an IADsLargeInteger object.

  12. Uses IADsLargeIntegerLowPart to determine if the low order 32-bits in objMaxPwdAge are equal to 0. If so, displays a message stating that the password does not expire and then quits. Otherwise, continues.

  13. Converts objMaxPwdAge from 100-nanosecond intervals to days and displays the maximum password age allowed in the domain.

  14. Checks if the time that has elapsed since the password was changed (stored in the intTimeInterval variable) is greater than or equal to the maximum password age allowed in the domain (stored in the dblMaxPwdDays variable).

  15. If intTimeInterval is greater than or equal to dblMaxPwdDays, displays a message stating that the password has expired. Otherwise, displays a message stating when the password will expire.

The script sample in Listing 6 shows the steps for determining if the password for the MyerKen user account expires. If the password is configured to expire, the script determines if expiration has already occurred. If it has not occurred, the script shows when expiration will occur.

Listing 6. Determining when or if a user account password expires

On Error Resume Next

Const ADS_UF_DONT_EXPIRE_PASSWD = &h10000
Const E_ADS_PROPERTY_NOT_FOUND  = &h8000500D
Const ONE_HUNDRED_NANOSECOND    = .000000100
Const SECONDS_IN_DAY            = 86400

Set objUser = GetObject("LDAP://CN=myerken,OU=management,DC=fabrikam,DC=com")

intUserAccountControl = objUser.Get("userAccountControl")
If intUserAccountControl And ADS_UF_DONT_EXPIRE_PASSWD Then     ' LINE 11
    WScript.Echo "The password does not expire."
    WScript.Quit
Else
    dtmValue = objUser.PasswordLastChanged
    If Err.Number = E_ADS_PROPERTY_NOT_FOUND Then               ' LINE 16
        WScript.Echo "The password has never been set."
        WScript.Quit
    Else
        intTimeInterval = Int(Now - dtmValue)
        WScript.Echo "The password was last set on " & _
          DateValue(dtmValue) & " at " & TimeValue(dtmValue)  & vbCrLf & _
          "The difference between when the password was last" & vbCrLf & _
          "set and today is " & intTimeInterval & " days"
    End If

    Set objDomain = GetObject("LDAP://DC=fabrikam,DC=com")
    Set objMaxPwdAge = objDomain.Get("maxPwdAge")

    If objMaxPwdAge.LowPart = 0 Then
        WScript.Echo "The Maximum Password Age is set to 0 in the " & _
                     "domain. Therefore, the password does not expire."
        WScript.Quit
    Else
        dblMaxPwdNano = _
            Abs(objMaxPwdAge.HighPart * 2^32 + objMaxPwdAge.LowPart)
        dblMaxPwdSecs = dblMaxPwdNano * ONE_HUNDRED_NANOSECOND  ' LINE 37
        dblMaxPwdDays = Int(dblMaxPwdSecs / SECONDS_IN_DAY)     ' LINE 38
        WScript.Echo "Maximum password age is " & dblMaxPwdDays & " days"

        If intTimeInterval >= dblMaxPwdDays Then
            WScript.Echo "The password has expired."
        Else
            WScript.Echo "The password will expire on " & _
              DateValue(dtmValue + dblMaxPwdDays) & " (" & _
              Int((dtmValue + dblMaxPwdDays) - Now) & " days from today)."
        End If
    End If
End If

Concluding With a Way to Impress the User Community

We've demonstrated how you can view password expiration information for a user account that you've specified in the script. How, then, can you retrofit the script to provide the currently logged-on user the same information?

Here's a hint: Modify the script so that it uses the ADSI IADsADSystemInfo utility interface's UserName and DomainDNSName properties, and incorporate the modified script into a logon script. Try it yourself first. If you don't have the time or just don't feel like trying, here's how to do it.

Displaying Password Expiration for the Currently Logged-On User

Listing 7 contains a script that displays password expiration information for the currently logged-on user. To carry out this task, the script performs the same steps as shown in Listing 6, with the exception of the following modifications:

  1. Uses the CreateObject function to create an instance of the ADSI ADSystemInfo utility object (line 8).

  2. Binds to the currently logged-on user using the GetObject function, the LDAP provider, and the ADSystemInfoUserName property (line 9).

    Rather than bind to a hard-coded user account name, the UserName property delivers the distinguished name of the currently logged-on user to the script.

  3. Binds to the current domain of the logged-on user using the GetObject function, the LDAP provider, and the ADSystemInfoDomainDNSName property (line 28).

The script sample in Listing 7 shows the steps for determining if the password for the currently logged-on user account expires. If the password is configured to expire, the script determines if expiration has already occurred. If it has not occurred, the script shows when expiration will occur.

Listing 7. Answer to the exercise

On Error Resume Next

Const ADS_UF_DONT_EXPIRE_PASSWD = &h10000
Const E_ADS_PROPERTY_NOT_FOUND  = &h8000500D
Const ONE_HUNDRED_NANOSECOND    = .000000100
Const SECONDS_IN_DAY            = 86400

Set objADSystemInfo = CreateObject("ADSystemInfo")              ' LINE 8
Set objUser = GetObject("LDAP://" & objADSystemInfo.UserName)   ' LINE 9

intUserAccountControl = objUser.Get("userAccountControl")
If intUserAccountControl And ADS_UF_DONT_EXPIRE_PASSWD Then
    WScript.Echo "The password does not expire."
    WScript.Quit
Else
    dtmValue = objUser.PasswordLastChanged
    If Err.Number = E_ADS_PROPERTY_NOT_FOUND Then
        WScript.Echo "The password has never been set."
        WScript.Quit
    Else
        intTimeInterval = Int(Now - dtmValue)
        WScript.Echo "The password was last set on " & _
          DateValue(dtmValue) & " at " & TimeValue(dtmValue)  & vbCrLf & _
          "The difference between when the password was last" & vbCrLf & _
          "set and today is " & intTimeInterval & " days"
    End If

    Set objDomain = GetObject("LDAP://" & objADSystemInfo.DomainDNSName)
    Set objMaxPwdAge = objDomain.Get("maxPwdAge")

    If objMaxPwdAge.LowPart = 0 Then
        WScript.Echo "The Maximum Password Age is set to 0 in the " & _
                     "domain. Therefore, the password does not expire."
        WScript.Quit
    Else
        dblMaxPwdNano = _
            Abs(objMaxPwdAge.HighPart * 2^32 + objMaxPwdAge.LowPart)
        dblMaxPwdSecs = dblMaxPwdNano * ONE_HUNDRED_NANOSECOND
        dblMaxPwdDays = Int(dblMaxPwdSecs / SECONDS_IN_DAY)
        WScript.Echo "Maximum password age is " & dblMaxPwdDays & " days"

        If intTimeInterval >= dblMaxPwdDays Then
            WScript.Echo "The password has expired."
        Else
            WScript.Echo "The password will expire on " & _
              DateValue(dtmValue + dblMaxPwdDays) & " (" & _
              Int((dtmValue + dblMaxPwdDays) - Now) & " days from today)."
        End If
    End If
End If

 

Scripting Clinic

Greg Stemp has long been acknowledged as one of the country's foremost authorities on scripting, and has been widely acclaimed as a world-class... huh? Well, how come they let football coaches make up stuff on their resumes? Really? He got fired? Oh, all right. Greg Stemp works at... Oh, come on now, can't I even say that? Fine. Greg Stemp gets paid by Microsoft, where he tenuously holds the title of lead writer for the System Administration Scripting Guide.

Dean Tsaltas is a Nova Scotian living in Redmond. He has become fluent in American and even chuckles at the accent of his friends and family back in the Maritimes. He got his start in computing at a tender age when his grandma and parents chipped in and bought him his beloved C-64 and a subscription to Compute!'s Gazette. He has been at Microsoft for a couple of years now and has a message for friends and family back home and in Vancouver: "No, I have not met Bill!"

Bob Wells wanders around aimlessly espousing the virtues of scripting to anyone who will listen. Rumor has it, Bob's two dachshunds know more about scripting than most humans. In his spare time, Bob contributes to the System Administration Scripting Guide.

Ethan Wilansky spends a lot of his work time writing and consulting. He's crazy about scripting, Yoga, gardening, and his family (not necessarily in that order). He is currently working on a way to create script that will take out the trash and wash the dinner dishes.