Recipe 6.23 Finding Users Whose Passwords Are About to Expire

6.23.1 Problem

You want to find the users whose passwords are about to expire.

6.23.2 Solution Using a command-line interface
> dsquery user -stalepwd <NumDaysSinceLastPwdChange> Using Perl
# This code finds the user accounts whose password is about to expire
# Domain and container/OU to check for accounts that are about to expire
my $domain   = '<DomainDNSName>';
my $cont     = ''; # set to empty string to query entire domain
                   # Or set to a relative path in the domain, e.g. cn=Users
# Days since password change 
my $days_ago = <NumDaysSinceLastPwdChange>  # e.g. 60;
# ------ END CONFIGURATION ---------

use strict;
use Win32::OLE;
   $Win32::OLE::Warn = 3;
use Math::BigInt;

# Need to convert the number of seconds from $day_ago
# to a large integer for comparison against pwdLastSet
my $past_secs = time - 60*60*24*$days_ago;
my $intObj = Math::BigInt->new($past_secs);
   $intObj = Math::BigInt->new($intObj->bmul('10 000 000'));
my $past_largeint = Math::BigInt->new(
                                  $intObj->badd('116 444 736 000 000 000'));
   $past_largeint =~ s/^[+-]//;

# Setup the ADO connections
my $connObj                         = Win32::OLE->new('ADODB.Connection');
$connObj->{Provider}                = "ADsDSOObject";
# Set these next two if you need to authenticate
# $connObj->Properties->{'User ID'}   = '<User>';     
# $connObj->Properties->{'Password'}  = '<Password>';
my $commObj                         = Win32::OLE->new('ADODB.Command');
$commObj->{ActiveConnection}        = $connObj;
$commObj->Properties->{'Page Size'} = 1000;
# Grab the default domain naming context
my $rootDSE = Win32::OLE->GetObject("LDAP://$domain/RootDSE");
my $rootNC = $rootDSE->Get("defaultNamingContext");
# Run ADO query and print results
$cont .= "," if $cont and not $cont =~ /,$/;
my $query  = "<LDAP://$domain/$cont$rootNC>;";
$query .=  "(&(objectclass=user)";
$query .=    "(objectcategory=Person)";
$query .=    "(!useraccountcontrol:1.2.840.113556.1.4.803:=2)";
$query .=    "(pwdLastSet<=$past_largeint)";
$query .=    "(!pwdLastSet=0));";
$query .=  "cn,distinguishedName;";
$query .= "subtree";
$commObj->{CommandText} = $query;
my $resObj = $commObj->Execute($query);
die "Could not query $domain: ",$Win32::OLE::LastError,"\n" 
   unless ref $resObj;

print "\nUsers who haven't set their passwd in $days_ago days or longer:\n";
my $total = 0;
while (!($resObj->EOF)) {
   print "\t",$resObj->Fields("distinguishedName")->value,"\n";
print "Total: $total\n";

6.23.3 Discussion

When a Windows-based client logs on to Active Directory, a check is done against the domain password policy and the user's pwdLastSet attribute to determine if the user's password has expired. If it has, the user is prompted to change it. In a pure Windows-based environment, this notification process may be adequate, but if you have a lot of non-Windows-based computers that are joined to an Active Directory domain (e.g., Kerberos-enabled Unix clients), or you have a lot of application and service accounts, you'll need to develop your own user password expiration notification process. Even in a pure Windows environment, cached logins present a problem because when a user logs into the domain with cached credentials (i.e., when the client is not able to reach a domain controller), this password expiration notification check is not done.

The process of finding users whose passwords are about to expire is a little complicated. Fortunately, the new dsquery user command helps by providing an option for searching for users that haven't changed their password for a number of days (-stalepwd). The downside to the dsquery user command is that it will not only find users whose password is about to expire, but also users that must change their password at next logon (i.e., pwdLastSet = 0). The Perl solution does not suffer from this limitation.

The Perl solution consists of a two-step process. First, we need to calculate a time in the past at which we would consider a password "old" or "about" to expire. The pwdLastSet attribute is a replicated attribute on user objects that contain the timestamp (as a large integer) of when the user last set her password. If today is May 31 and we want to find all users who have not set their password for 30 days, we need to query for user's who have a pwdLastSet timestamp older than May 1.

First, a brief word on timestamps stored as large integers. It may seem odd, but large integer timestamps are represented as the number of 100-nanosecond intervals since January 1, 1601. To convert the current time to a large integer, we have to find the current time in seconds since the epoch (January 1, 1970) multiply that times 10,000,000 and then add 116,444,736,000,000,000 to it. This will give you an approximate time (in 100-nanosecond intervals) as a large integer. It is only an approximate time because when dealing with big numbers like this, a degree of accuracy is lost during the arithmetic.

I chose to use Perl over VBScript because VBScript doesn't handle computing large integers given the current time and date very well.

All right, now that you know how to calculate the current time, we need to calculate a time in the past as a large integer. Remember, we need to find the time at which passwords are considered close to expiring. In the Perl solution, you can configure the number of days since users changed their password. Once we've calculated this value, all we need is to come up with a search filter that we can use in ADO to find the matching users.

The first part of the filter will match all user objects.

$query .= "(&(objectclass=user)";
$query .= "(objectcategory=Person)";

But we really only want to find all enabled user objects (do you care if a disabled user object's password is about to expire?). This next bit-wise filter will match only enabled user objects. See Recipe 6.13 for more information on finding disabled and enabled users.

$query .= "(!useraccountcontrol:1.2.840.113556.1.4.803:=2)";

The next part of the filter is the important part. This is where we use the derived last password change timestamp to compare against pwdLastSet.

$query .= "(pwdLastSet<=$past_largeint)";

Finally, we exclude all users that are required to change their password at next logon (pwdLastSet equal to zero).

$query .= "(!pwdLastSet=0));";

6.23.4 See Also

Recipe 6.11 for more on the password policy for a domain, Recipe 6.17 for how to set a user's password, and Recipe 6.22 for how to set a user's password to never expire

    Chapter 3. Domain Controllers, Global Catalogs, and FSMOs
    Chapter 6. Users
    Appendix A. Tool List