In Active Directory Users and Computers you can specify the date when a user account expires on the "Account" tab of the user properties dialog. This date is stored in the accountExpires attribute of the user object. There is also a property method called AccountExpirationDate, exposed by the IADsUser interface, that can be used to display and set this date. If you’ve ever compared accountExpires and AccountExpirationDate with the date shown in ADUC, you may have wondered what’s going on. It is common for the values to differ by a day, sometimes even two days.

On the "Account" tab in ADUC there is a section labeled "Account expires". You can select either "Never" or "End of". If you select "End of" you can pick a date. Presumably the account will expire at midnight that day, local time.

The accountExpires attribute of the user object is data type Integer8. Integer8 values are 64-bit (8-byte) numbers representing dates as the number of 100-nanosecond intervals (also called ticks) since 12:00 AM January 1, 1601. One 100-nanosecond interval is 0.0000001 seconds. It sounds like that should be a huge number, and that’s why it requires a 64-bit value. 12:00 AM January 1, 2006, works out to be 127,805,472,000,000,000 100-nanosecond intervals since 12:00 AM January 1, 1601.

You may recall that years divisible by 100 are not leap years, unless they are also divisible by 400. The year 1900 was not a leap year, but 2000 was. The year 1600 was a similar exception, so it was a leap year. I think Microsoft selected their "zero" date for Integer8 values to avoid dealing with February 29, 1600. This also avoids the year 1582 when the switch was made from the Julian to the Gregorian calendar. October 4, 1582, was followed by October 15.

Another complication is that the date represented by accountExpires is in Coordinated Universal Time, referred to as UTC for the French acronym. This used to be called Greenwich Mean Time. To convert to local time, you must adjust for your time zone. A program can use the time zone bias stored in the local machine registry. This adjustment may not sound like a big deal, but when you configure an account to expire at the end of the day April 1, 2007, in the Central Time Zone of the United States (where the time zone bias is 5 hours when Daylight Savings is in affect), the value stored in accountExpires corresponds to 5:00 AM April 2, 2007 UTC.

The AccountExpirationDate property method is a holdover from NT domains, so it retains a few quirks. First, the "zero" date in NT domains was January 1, 1970. Any date that was undefined, or had the default zero value, was interpreted as 12:00 AM January 1, 1970. If a user object is configured to never expire, and the accountExpires attribute has a value of 0, AccountExpirationDate interprets this as January 1, 1970. I’m sure this is hard coded in the property method. The date January 1, 1970, has no significance in Active Directory.

If a user object in Active Directory has never had an expiration date, the accountExpires attribute is set to a huge number. The actual value is 2^63 – 1, or 9,223,372,036,854,775,807. This is because 64-bit numbers can range from -2^63 to 2^63 - 1, making this the largest number that can be saved as a 64-bit value. Obviously this represents a date so far in the future that it cannot be interpreted. In fact, AccountExpirationDate raises an error if it attempts to read this value. If a user object has an expiration date, and then you remove this date in ADUC by selecting "Never" on the "Account" tab, the GUI sets accountExpires to 0. Thus, the values 0 and 2^63 - 1 both really mean "Never".

The only values that you can assign to the accountExpires attribute in VBScript are 0 and -1. Because of the way 64-bit values are handled, -1 is actually 2^63 - 1, the huge number I referred to. The value 0 corresponds to the date/time 12:00 AM January 1, 1601. The value -1 corresponds to September 14 of the year 30,828.

ADSI provides the IADsLargeInteger interface to deal with Integer8 (64-bit values). This interface treats Integer8 values as an object and provides two methods, the HighPart and LowPart methods. These methods break up the 64-bit value into high and low 32-bit parts that can be handled by VBScript. The Integer8 value is equal to the value returned by the HighPart method times 2^32 plus the value returned by the LowPart method. In VBScript we can code:

Set objDate = objUser.accountExpires
lngDate = (objDate.HighPart * (2^32)) + objDate.LowPart

The value of the variable lngDate will be the number of 100-nanosecond intervals since 12:00 AM January 1, 1601. To convert to a date we divide the value of lngDate by 10^7 to convert to seconds, then use the DateAdd function to add this number of seconds to the date January 1, 1601. This calculation must be adjusted for the time zone bias to convert from UTC to local time.

VB and VBScript can only handle 15 significant digits (All integers up to 2^53 are represented exactly). However, the value of the variable lngDate above will typically have 18 digits, so the last 3 digits will be zeros. This is not a problem, as we are still accurate to the nearest 1000 100-nanosecond intervals, which is 0.0001 second.

Now a final complication. The IADsLargeInteger interface has a bug. Because of the way 32-bit numbers are handled, the LowPart method can return negative values. This is fine, but when this happens, the calculation above is wrong. To compensate for the bug, you need to increase the value returned by the HighPart method by one whenever the value returned by the LowPart method is negative. This adjustment corresponds to 2^32 100-nanosecond intervals, which is 7 minutes 9.5 seconds. That’s not much when determining when an account expires, but it matters for other Integer8 values. Plus it adds to the confusion when you calculate the dates.

The AccountExpirationDate property method can read and write values. To assign an expiration date of 3:30 PM on April 22, 2007, you would use VBScript code similar to:

Set objUser = GetObject("LDAP://cn=Jim Smith,ou=Sales,dc=MyDomain,dc=com")
objUser.AccountExpirationDate = #04/22/2007 15:30#

The "Account" tab of the user property dialog of ADUC will show the expiration date as "End of: Saturday April 21, 2007". The actual expiration date will be 3:30:00 PM on April 22, 2007. This will be in the time zone of the computer where the VBScript code was run. The property method assigns the correct 64-bit value to the accountExpires attribute, corresponding to the date and time in UTC.

When ADUC shows an expiration date, it means at the end of that day. This really means any time during the next day. For example, if ADUC shows the expiration date as "End of: Saturday April 21, 2007", this really means April 21, 2007 24:00, which is the same as April 22, 2007 00:00. An actual expiration date of April 22, 2007 10:15 AM will also show as "End of: April 21, 2007" in ADUC. In fact, an expiration date of April 22, 2007 23:59:59 still shows as "End of: April 21, 2007" in ADUC.

The accountExpires attribute, the AccountExpirationDate property method, and the expiration date shown in ADUC all fail to account for daylight savings time changes. This can cause the actual expiration date to differ by one hour from the time you expect. For example, assume the current date is May 25, 2007, and you are in the Central Time Zone of the United States. Since Daylight Savings is in affect, the active time zone bias is 5 hours. If you use the AccountExpirationDate property method to assign the date December 10, 2007, for an account to expire, the accountExpires attribute will be assigned a value corresponding to December 10, 2007 5:00 AM (UTC). However, Daylight Savings Time will not be in affect in December, so the active time zone bias at that time will be 6 hours. In December the UTC date of December 10, 2007 5:00 AM will be converted to December 9, 2007 11:00 PM local time, one hour earlier than expected. ADUC will show "End of: December 8, 2007", which appears to be wrong by two days.

Another way the expiration date can seem to be wrong by two days is if you assign an expiration date in one time zone, then view the expiration date in another time zone. The actual value saved in the accountExpires attribute is always in UTC, so if the active time zone bias in affect when the value is saved is different from the active time zone bias when the value is read, there will be a discrepancy. If you take this into account, you can always assign a value that will be correct at the time of account expiration, in the time zone you desire.