User Management with Active Directory—How Password Modification Dates Are Stored


Jump to: navigation, search
Visual C# Tutorials
C# Tutorials

User Management

© 2006 Pearson Education, Inc.

How Password Modification Dates Are Stored

Active Directory and ADAM use the pwdLastSet attribute to record when a password was last changed, via either an end-user password change or an administrative reset. Like most time-based Windows data in the directory, the attribute uses the 2.5.5.16 LargeInteger attribute syntax, which essentially holds a Windows FILETIME structure as an 8-byte integer. We already discussed how to read and write these attributes in Chapter 6, as well as build LDAP search filters based on them in Chapter 4, so that knowledge should be easy to apply to this problem.

There is one edge case that developers should be aware of when dealing with this attribute. Namely, the pwdLastSet attribute can be set to zero (0), which implies that the password is automatically expired and must be changed at next login.

NOTE

Zero Is Not a Valid FILETIME Value in .NET

The 0 value for pwdLastSet is especially important to remember because it is not a valid FILETIME value. If we pass it to a .NET function that converts between .NET DateTime structures and FILETIME, it will throw an error. Additionally, if pwdLastSet is 0, the user cannot bind to the directory via LDAP. This makes it impossible to do programmatic password changes via LDAP. Only administrative resets with different credentials are possible in this state.

Now that we know the details on how this mechanism works, we are ready to write some code to check this. The first thing we need is a user’s pwdLastSet value as a .NET Int64, or long integer. As per Chapter 6, we can do this using DirectorySearcher and its built-in marshaling of the data, or we can use one of the conversion functions we described for use with DirectoryEntry. For our purposes, we will use DirectorySearcher for converting the LargeInteger values in conjunction with Listing 10.7 to obtain domain policies.

We will step through a larger class we have chosen to name PasswordExpires, explaining as we go the thought process that surrounds what we are trying to accomplish. As such, we might have to refer to previous listings to see any member variables. This is a complete class and it requires a number of lines, but don’t worry about needing to copy it verbatim. We will include it as a sample on the book’s web site under its listing number.

The first part of determining password expiration is to determine our domain policy for the maximum password age (MaxPwdAge). Listing 10.8 shows how we can easily accomplish this using the DomainPolicy class we introduced inListing 10.7 along with some tricks we learned in Chapter 9 using System.DirectoryServices.ActiveDirectory (SDS.AD) classes.

Listing 10.8: PasswordExpires, Part I

public class PasswordExpires
{
  DomainPolicy policy;
  
  const int UF_DONT_EXPIRE_PASSWD = 0x10000;
 
  public PasswordExpires()
  {
    //get our current domain policy
    Domain domain = Domain.GetCurrentDomain();
    DirectoryEntry root = domain.GetDirectoryEntry();
  
    using (domain)
    using (root)
    {
      this.policy = new DomainPolicy(root);
    }
  }

In Listing 10.8, we are simply using the Domain class from SDS.AD to get a DirectoryEntry object bound to the current domain’s default naming context. We need the root partition of the domain in order to determine our domain policies. At this point, we simply load our DomainPolicy object with our root domainDNS object. Next, we need to calculate the actual DateTime when a user’s password would expire. Listing 10.9 shows how we can accomplish this.

Listing 10.9: PasswordExpires, Part II

public DateTime GetExpiration(DirectoryEntry user)
  {
    int flags = 
      (int)user.Properties["userAccountControl"][0];
 
    //check to see if password is set to expire
    if(Convert.ToBoolean(flags & UF_DONT_EXPIRE_PASSWD))
    {
      //the user’s password will never expire
      return DateTime.MaxValue;
    }
 
    long ticks = GetInt64(user, "pwdLastSet");
 
    //user must change password at next login
    if (ticks == 0)
      return DateTime.MinValue;
 
    //password has never been set
    if (ticks == -1)
    {
      throw new InvalidOperationException(
        "User does not have a password"
        );
    }
 
    //get when the user last set their password;
    DateTime pwdLastSet = DateTime.FromFileTime(
      ticks
      );
 
    //use our policy class to determine when
    //it will expire
    return pwdLastSet.Add(
      this.policy.MaxPasswordAge
      );
  }

The first thing we do in Listing 10.9 is check to see if the user’s account is set so that the password never expires. We will use the convention that DateTime.MaxValue means it will never expire. Next, we are using a helper function called GetInt64 (see Listing 10.10). This function marshals the user’s pwdLastSet attribute into an Int64 for us using a DirectorySearcher object. Notice that one of three conditions can arise out of this check. First, the pwdLastSet attribute might be null (if it is not set on the object), in which case the user account has no password. We chose to treat the situation where a user does not have a password as an error condition, but this can differ by application. Second, the attribute might be 0, which means that the user must change her password at the next logon. We chose to use the convention that DateTime.MinValue meant that the password must be changed at next log on. Lastly, the attribute might contain some value that we can interpret as a FILETIME structure and can convert using DateTime.FromFileTime. The calculation for determining when a user’s password will expire is simple. We just add the DateTime value of when the user last changed her password to the TimeSpan value of the domain’s MaxPwdAge policy. If the user’s password has already expired, we will still get a DateTime value, but it will be in the past.

Knowing the date a user’s password has expired is nice, but we might actually want to know how much time is left before the user’s password expires. That is a very easy calculation, as Listing 10.10 demonstrates.

Listing 10.10: PasswordExpires, Part III

public TimeSpan GetTimeLeft(DirectoryEntry user)
  {
    DateTime willExpire = GetExpiration(user);
 
    if (willExpire == DateTime.MaxValue)
      return TimeSpan.MaxValue;
 
    if (willExpire == DateTime.MinValue)
      return TimeSpan.MinValue;
 
    if (willExpire.CompareTo(DateTime.Now) > 0)
    {
      //the password has not expired
      //(pwdLast + MaxPwdAge)- Now = Time Left
      return willExpire.Subtract(DateTime.Now);
    }
 
    //the password has already expired
    return TimeSpan.MinValue;
  }
  
  private Int64 GetInt64(DirectoryEntry entry, string attr)
  {
    //we will use the marshaling behavior of
    //the searcher
    DirectorySearcher ds = new DirectorySearcher(
      entry,
      String.Format("({0}=*)", attr),
      new string[] { attr },
      SearchScope.Base
      );
      
    SearchResult sr = ds.FindOne();
    
    if (sr != null)
    {
      if (sr.Properties.Contains(attr))
      {
        return (Int64)sr.Properties[attr][0];
      }
    }
    return -1;
  }

We chose to return a TimeSpan value representing the time left before a user’s password would expire in Listing 10.10. If either TimeSpan.MaxValue or TimeSpan.MinValue is returned, it is meant to indicate that the user’s password does not expire or has already expired, respectively. For completeness, we have also included our helper GetInt64 in Listing 10.10, though by now we know that everyone is probably aware of how to marshal LargeInteger values from Chapter 6.

We typically want to do something based on when a password will expire, so it is important to know how much time is left. However, if we just want to know whether an account has expired, we can use the previously mentioned msDS-User-Account-Control-Computed attribute for Windows 2003 Active Directory and ADAM, or the aptly named msDS-UserPasswordExpired attribute for ADAM, to just give us a yes/no answer. Listing 10.11 shows one such example.

Listing 10.11: Checking Password Expiration

string adsPath = "LDAP://CN=User1,OU=Users,DC=domain,DC=com";
 
DirectoryEntry user = new DirectoryEntry(
  adsPath,
  null,
  null,
  AuthenticationTypes.Secure
  );
 
string attrib = "msDS-User-Account-Control-Computed";
 
using (user)
{
  user.RefreshCache(new string[] { attrib });
 
  int flags = (int)user.Properties[attrib].Value
    & (int)AdsUserFlags.PasswordExpired);
 
  if (Convert.ToBoolean(flags)
  {
    //password has expired
    Console.WriteLine("Expired");
  }
}

Of course, the problem with something like Listing 10.11 is that we don’t know when the password actually expired or how much time is left before it does expire. Additionally, as we previously mentioned, a solution like this will work with only Windows 2003 Active Directory and ADAM. Windows 2000 Active Directory users must use a solution such as that shown in Listing 10.8.


Previous_Page_.gif Next_Page_.gif





Personal tools