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#
objUser.SetInfo
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.