Kiến thức tổng hợp về AD Development


User Management with Active Directory—Finding Users

Finding Users

When we speak of user objects for the remainder of this chapter, we are really talking about the user class in both Active Directory and ADAM. This is the class that acts as a security principal for Active Directory (and often, for ADAM). It is typically the class that we care the most about. The first thing we are likely to want to do with user accounts in Active Directory is find them. Given all that we know about searching from Chapters 4 and 5, this should be easy. Essentially, we just need to know where we want to search and what filter to use to find users.

Let’s start with the LDAP filter. Our goal is to build an LDAP filter that will find exactly what we want and that will be as efficient as possible. A few attributes in Active Directory distinguish user objects from other object types that we can use to build a filter:

  • objectCategory
  • objectClass
  • sAMAccountName
  • sAMAccountType

The objectCategory attribute has the advantage of being single-valued and indexed by default on all versions of Active Directory. This attribute is meant to be used to group common types of objects together so that we can search across all of them. Many of the schema classes related to users share the same value of person as their object category. While this is useful for searches in which we want to find information across many different types of user-related objects, it is not as useful for finding the user objects we typically care about (which are usually security principals). For example, in Active Directory, since both user and contact classes share the same objectCategory value of person, it alone will not tell them apart.

The objectClass attribute seems like a no-brainer, as (objectClass=user) will always find user objects exclusively. The problem here is that in many forests, the objectClass attribute is not indexed and it is always multivalued. As we know from the section titled Optimizing Search Performance, in Chapter 5, we generally want to try to avoid searches on nonindexed attributes, so using objectClass alone in a filter might not be the most efficient solution. This is why we see a lot of samples that search for user objects using an indexed filter like this:

(&(objectCategory=person)(objectClass=user))

This will get the job done, but we can do even better than this.

NOTE

Behavior Change in Windows Server 2003

Windows Server 2003 Active Directory indexes the objectClass attribute by default, so this rule really only applies to Windows 2000 Active Directory now. Additionally, the schema administrators for your domain may have already indexed it, so check the schema before making assumptions.

One key difference between contact objects and user objects in Active Directory is that user objects have a sAMAccountName attribute that is indexed by default. Thus, we could build a filter like this:

(&(objectCategory=person)(sAMAccountName=*))

This will separate the contacts from the users effectively. However, another approach is available that can find user objects directly, and it may be the most efficient technique for Active Directory:

(sAMAccountType=805306368)

Using the sAMAccountType attribute with a value of 805306368 accesses a single-valued, indexed attribute that uniquely defines user objects. The only downside here is that this attribute is not well documented, so it may not be recommended by Microsoft. However, in our investigations, it is effective.

One piece of good news is that all of these attributes are included in the global catalog, so we can use all of these filters there as well.

This should provide a foundation to build on for various user object searches. We may also wish to combine these filters with other attributes to find different subsets of user objects matching specific criteria. We will see some examples of that in the rest of this section.

User Management with Active Directory—Finding Users in ADAM

Finding Users in ADAM

Things change a little bit for ADAM, because we don’t have things like sAMAccountName or sAMAccountType on an ADAM user. These attributes are actually from the auxiliary class called securityPrincipal. This class happens to be slightly different depending on whether it comes from Active Directory or ADAM. In this case, the securityPrincipal class does not contain sAMAccountName or related attributes in ADAM.

This means that we need to adjust our filters slightly to find our users in ADAM. It turns out that we can use some other indexed attributes, such as userPrincipalName, to find our users. However, since this attribute is not required, we have to be careful and ensure that all of our user objects have set a value for userPrincipalName.

As mentioned in Chapter 8, ADAM also includes the idea of a userProxy object that we often will want to return as well. To do so, we should look for common attributes found on both classes that would filter them for our needs. In this case, we know that both the user and userProxy classes contain userPrincipalName, so it is a good candidate for this purpose. We could use a simple filter such as (userPrincipalName=*) if we knew that all of our objects would have a value set for this attribute. However, since not all user or userProxy objects might have a value set for userPrincipalName, we would have to use objectCategory to be safe. For example:

(|(objectCategory=person)(objectCategory=userProxy))

The implications here are that if any other class in the directory also shared the same objectCategory of person or userProxy, they would also be returned. In default ADAM instances, this does not occur, but we should always check with our particular ADAM instance. Now, if we want to separate the two classes and treat them differently, we can use objectClass, as the classes are different for each type:

(&(objectClass=user)(objectCategory=person))
(&(objectClass=userProxy)(objectCategory=userProxy))

The first filter will return only user objects using objectCategory as an index, and the second will return only userProxy objects in the same manner. It is important to remember that we can customize ADAM heavily with application-specific classes. We should never rely on the fact that any particular class will be there or even be indexed. As such, these are only guidelines, and we really need to check the particular ADAM instance’s schema ourselves to determine what is there.

NOTE

New Versions of ADAM Include the userProxyFull Class

With later versions of ADAM, a new class called userProxyFull can be found that essentially mirrors the user class with the addition of the msDS-BindProxy auxiliary class. This means that we cannot use objectCategory to distinguish between user and userProxyFull classes as we can with userProxy classes. We must instead rely upon the objectClass to distinguish between the two. This is another example of where we need to be cognizant of what our schema is for ADAM.

 

User Management with Active Directory—Creating Users

Creating Users

Creating users in Active Directory is actually just like creating any other object in the directory. We simply create an instance of the class we want and set any required attributes on the object. However, to get a fully functional user object that is “enabled,” has a password, and has other useful attributes takes a bit more work.

But first, the basics! Let’s create a user object in an organizational unit called people that exists in the root of our domain (seeListing 10.1).

Listing 10.1: Creating an Active Directory or ADAM User

DirectoryEntry parent = new DirectoryEntry(
  "LDAP://OU=people,DC=mydomain,DC=com",
  null,
  null,
  AuthenticationTypes.Secure
  );
 
DirectoryEntry user = 
  parent.Children.Add("CN=test.user", "user");
 
using (user)
{
  //sAMAccountName is required for W2k AD, we would not use
  //this for ADAM, however.
  user.Properties["sAMAccountName"].Value = "test.user";
  
  //userPrincipalName is not required, but recommended
  //for ADAM. AD also contains this, so we can use it.
  user.Properties["userPrincipalName"].Value = "test.user";
  user.CommitChanges();
}

As Listing 10.1 demonstrates, creating a user is pretty simple. We just need to know the distinguished name (DN) of the object that will serve as the user’s parent container. This is typically an organizational unit, but it could be a container or a lot of different things in ADAM. We should also note that ADAM is slightly different from Active Directory in that sAMAccountName does not exist in ADAM, so there are actually no required attributes! Instead, we chose to populate userPrincipalName (which is a good idea in both Active Directory and ADAM). The userPrincipalName attribute is one of the accepted names used for binding to the directory.

If we remember from Chapter 3, we know that creating an object requires supplying a relative distinguished name (RDN) value. The prefix that we should use is defined in the schema by the rDNAttId attribute on the class. Active Directory and ADAM tend to use the CN RDN prefix for a number of object types; however, it is not uncommon for third-party LDAP directories from other vendors to use something different (e.g., UID). The RDN prefix (e.g., CN, OU, O, etc.) will differ slightly by object, but for user objects, we should generally choose the CN prefix. Essentially, we must always supply the name using the correct RDN attribute name for the class being created.

In Listing 10.1, we set the sAMAccountName attribute. This is not strictly required in the Windows 2003 version of Active Directory, but it is in the Windows 2000 version. Windows 2003 Active Directory will pick a random name for us, though, so it is probably a good idea to set one intentionally, especially since this is the user’s login name in the Windows NT format (domain\login).

While the code in Listing 10.1 will create a user object in Active Directory (and with slight modification, in ADAM) it will not be very useful. The user account will not have a password, it will contain very few attributes, and it will not even be enabled by default. While some of these attributes are simple strings that do not require anything particularly complicated to set, passwords and account properties are more complicated. The sections that follow detail how to deal with them and make user accounts behave like the user accounts we have come to expect.

 

User Management with Active Directory—Managing User Account Features

Managing User Account Features

Creating user objects in Active Directory is not difficult. As we have seen, with the right permissions, we can create a basic user account in just a few lines of code.

However, getting user objects to behave like Windows user accounts is a bit more challenging. Windows accounts have many features, such as enabled/disabled status, names and identifications used for security and email, password management, and expiration and lockout status, all of which require more-intimate knowledge of how things work under the hood. The next several sections explore this in detail.

Managing Basic User Account Properties in Active Directory

Many of the important behaviors associated with a Windows account in Active Directory, such as enabled/disabled status, are controlled by an attribute called userAccountControl. This attribute contains a 32-bit integer that represents a bitwise enumeration of various flags that control account behavior.

These flags are represented in ADSI by an enumerated constant called ADS_USER_FLAG. Because this enumeration is so important in terms of working with user objects in System.DirectoryServices (SDS), we will convert the ADSI enumeration into a .NET-style enumeration, as shown in Listing 10.2.

Listing 10.2: User Account Control Flags

[Flags]
public enum AdsUserFlags
{ 
  Script = 1,                          // 0x1
  AccountDisabled = 2,                 // 0x2
  HomeDirectoryRequired = 8,           // 0x8 
  AccountLockedOut = 16,               // 0x10
  PasswordNotRequired = 32,            // 0x20
  PasswordCannotChange = 64,           // 0x40
  EncryptedTextPasswordAllowed = 128,  // 0x80
  TempDuplicateAccount = 256,          // 0x100
  NormalAccount = 512,                 // 0x200
  InterDomainTrustAccount = 2048,      // 0x800
  WorkstationTrustAccount = 4096,      // 0x1000
  ServerTrustAccount = 8192,           // 0x2000
  PasswordDoesNotExpire = 65536,       // 0x10000
  MnsLogonAccount = 131072,            // 0x20000
  SmartCardRequired = 262144,          // 0x40000
  TrustedForDelegation = 524288,       // 0x80000
  AccountNotDelegated = 1048576,       // 0x100000
  UseDesKeyOnly= 2097152,              // 0x200000
  DontRequirePreauth= 4194304,         // 0x400000
  PasswordExpired = 8388608,           // 0x800000
  TrustedToAuthenticateForDelegation = 16777216, // 0x1000000
  NoAuthDataRequired = 33554432        // 0x2000000
}

As we look through the members of this enumeration, we see a variety of words we associate with Windows accounts, such as AccountDisabled and PasswordNotRequired (the last one we hope you never use!). We also see some flags that we probably do not recognize, such as MnsLogonAccount and UseDesKeyOnly. For the most part, the esoteric flags are not important in daily account management tasks, so we can ignore them. Chances are, if we need these flags we are probably quite aware of them already.

The important thing to note is that even though 21 flags are currently defined for use with the userAccountControl attribute, Active Directory does not actually use all of them! Specifically, the ones that are not meaningful to Active Directory are

  • AccountLockedOut
  • PasswordCannotChange
  • PasswordExpired

Active Directory actually uses different mechanisms to control these account properties, so do not try to read them from userAccountControl! We discuss how to deal with the special cases in the upcoming sections.

User Management with Active Directory—Reading User Account Properties

Reading User Account Properties

Reading the userAccountControl attribute is simple. Listing 10.3 uses an enumeration we defined in Listing 10.2.

Listing 10.3: Reading the userAccountControl Attribute

DirectoryEntry user = new DirectoryEntry(
  "LDAP://CN=User1,CN=users,DC=domain,DC=com",
  null,
  null,
  AuthenticationTypes.Secure
  );
 
AdsUserFlags userFlags = (AdsUserFlags)
  user.Properties["userAccountControl"].Value;
 
Console.WriteLine(
  "AdsUserFlags for {0}: {1}",
  user.Path,
  userFlags
  );

This will generally write NormalAccount to the console for a typical user and may include other flags as well, depending on our specific deployment. If the account is disabled, AccountDisabled will be displayed in addition.

For Windows Server 2003 Active Directory, we could also use the msDS-User-Account-Control-Computed attribute in place of userAccountControl. Since it is constructed, we would need to use our RefreshCache technique from Chapter 3. However, the one benefit of using this attribute is that the three flags we previously mentioned as not being used with userAccountControl would actually be used and be accurate. This is not an option with Windows 2000 Server Active Directory installations—they will always need to use userAccountControl for reading user account properties.

 

User Management with Active Directory—Writing User Account

Properties

Writing User Account Properties

Writing values is equally as easy as reading them. We just create an integer value representing the proper combination of flags and overwrite the existing userAccountControl value, as shown in Listing 10.4.

Listing 10.4: Writing Account Values

DirectoryEntry entry = new DirectoryEntry(
  "LDAP://CN=some user,"
    + "CN=users,DC=mydomain,DC=com",
  null,
  null,
  AuthenticationTypes.Secure
  );
 
AdsUserFlags newValue = AdsUserFlags.NormalAccount
  | AdsUserFlags.DontExpirePassword;
 
entry.Properties["userAccountControl"]
  .Value = newValue;
entry.CommitChanges()

The trick here is that we must use valid combinations of flag values. In addition, other aspects of the account or the policies in effect may prevent certain values from being set.

A classic example that can trip up new developers happens when enabling an account by “unsetting” the AccountDisabled flag. In many domains, a minimum password length is required for all user accounts (and hopefully this is what you use as well). An account cannot be enabled unless it has a password. Therefore, we must set a valid password before enabling the account.

As a result, many typical provisioning processes that create accounts follow this protocol.

  1. Create the account with initial values and commit changes.
  2. Use the SetPassword operation to set an initial password.
  3. Enable the account and commit changes again.

Keep this in mind when creating and provisioning user accounts if errors occur.

User Management with Active Directory—Delegation Settings

Delegation Settings

Three flags in the enumeration relate to delegation, which we discussed in Chapter 8. Specifically, TrustedForDelegation and TrustedToAuthenticateForDelegation are used by service accounts that will be allowed to delegate users’ credentials to other machines. The difference between them is that TrustedForDelegation is the “unconstrained” delegation setting that works with Windows 2000 Server, and it represents the flag used when only Kerberos authentication is allowed in constrained delegation. TrustedToAuthenticateForDelegation is new with Windows Server 2003 and is used when delegation from any protocol is allowed. This is known as the “Protocol Transition” setting.

Finally, AccountNotDelegated is used to flag an account as “sensitive and cannot be delegated.” This is typically used on highly privileged accounts such as those used by directory administrators, where we would probably not want their account to be delegated by another service due to the security risk it poses.

 

Managing Basic User Account Properties in ADAM

ADAM works differently than Active Directory in that it does not rely on the userAccountControl attribute to maintain important account properties. Instead, Microsoft introduced a number of attributes prefixed with ms-DS- or msDS to hold this information:

  • ms-DS-UserAccountAutoLocked*
  • msDS-User-Account-Control-Computed*
  • msDS-UserAccountDisabled
  • msDS-UserDontExpirePassword
  • ms-DS-UserEncryptedTextPasswordAllowed
  • msDS-UserPasswordExpired*
  • ms-DS-UserPasswordNotRequired

The * in the preceding list indicates that the attribute is constructed.

With the exception of the integer-valued msDS-User-Account-Control-Computed, these attributes are Boolean values in the directory. Some of these attributes are also constructed attributes and as such, they cannot be written. We wish we could say there was some method behind the slightly different ldapDisplayName prefix values, but it just appears that it was overlooked.

Reading User Account Properties in ADAM

The constructed attribute called msDS-User-Account-Control-Computed takes the place of userAccountControl in ADAM. This attribute is new to Windows Server 2003 Active Directory and ADAM and we can use it rather than userAccountControl to read account properties. However, given that this is a constructed attribute, we can neither search on any values held within it nor set any values on this attribute. Listing 10.5 demonstrates how similar it is to read this attribute compared to using the userAccountControl attribute in Listing 10.3.

Listing 10.5: Reading the msDS-User-Account-Control-Computed Attribute

DirectoryEntry user = new DirectoryEntry(
  "LDAP://CN=User1,CN=users,DC=domain,DC=com",
  null,
  null,
  AuthenticationTypes.Secure
  );
 
//this is a pain to type a lot :)
string msDS = "msDS-User-Account-Control-Computed";
 
using (user)
{
  //this is constructed attribute
  user.RefreshCache(
    new string[]{msDS}
    ); 
 
  AdsUserFlags userFlags = 
    (AdsUserFlags)user.Properties[msDS].Value;
 
  Console.WriteLine(
    "AdsUserFlags for {0}: {1}",
    user.Path,
    userFlags
    );
}

We should note that the msDS-User-Account-Control-Computed attribute will accurately hold values like AccountLockedOut, PasswordCannotChange, and PasswordExpired. This departs from the userAccountControl attribute, where these values are not represented accurately because the flags are not used. We also have the option of using the special “alias” attributes such as ms-DS-UserAccountAutoLocked here. We simply read the Boolean value they return. In many cases, this may be more straightforward.

Writing User Account Properties in ADAM

Since the userAccountControl attribute is not used with ADAM, and its equivalent but constructed attribute cannot be written, we need to use the other msDS and ms-DS attributes to actually set values. Listing 10.6 shows one such example where we can enable or disable an ADAM account.

Listing 10.6: Writing Account Values

string adsPath = 
  "LDAP://localhost:389/"
  + "CN=User1,OU=Users,O=dunnry,C=US";
 
DirectoryEntry user = new DirectoryEntry(
  adsPath,
  null,
  null,
  AuthenticationTypes.Secure
  );
 
string attrib = "msDS-UserAccountDisabled";
 
using (user)
{
  //disable the account
  user.Properties[attrib].Value = true;
  user.CommitChanges();
}

As writing each of the other nonconstructed Boolean account properties is exactly the same, we will not demonstrate further examples. The key point to take away here is that we need to look to these attributes in lieu of using the userAccountControl attribute for ADAM.

NOTE

Boolean Attributes Can Accept String Values as Well

Boolean syntax attributes can accept both the .NET Boolean true and false as well as the LDAP string equivalent TRUE and FALSE (note the case). This is because the underlying value is held as a string in the LDAP directory and only marshaled as a Boolean for us to use in .NET. If we remember that searching for Boolean attributes requires using TRUE and FALSE, all of this stuff starts to make sense. Since using the actual Boolean type in .NET tends to be easier, we only mention it in passing as a fun factoid.

 

Determining Domain-Wide Account Policies

When working with user accounts in Active Directory, it is common to need to refer to domain-wide account policies. For example, policies such as the minimum and maximum password age and the minimum password length, as well as lockout policy, are determined at the domain level and apply to each user object in the domain.

All of the values are stored directly in the domain root object (not in RootDSE, but in the object pointed to by the defaultNamingContext attribute in RootDSE) as a set of attributes such as maxPwdAge, minPwdLength, and lockoutThreshold. Additionally, the password complexity rules are encoded in an enumerated value in the pwdProperties attribute.

These values tend to be quite static in most domains, so we would typically want to read these values only once per program execution. To make the policy values easy to consume, we show in Listing 10.7 a wrapper class for the domain account policies that converts all of the values into convenient .NET data types, such as TimeSpan. A special .NET enumeration type for the types of the password policy is also included. We won’t be able to include all of the class properties in the book, as that would take too much space, but we will have the full class available on the book’s web site.

We will refer to this sample in future discussions when demonstrating how to determine an account’s lockout status and for finding accounts with expiring passwords. It is also worthy to note that any LargeInteger values in these policy attributes are stored as negative values. We chose to invert them back to positive values because it is easier to think about them in this way. Developers choosing to use these attributes should keep this in mind, as it will throw off calculations later if not accounted for.

Listing 10.7: Determining Domain Policies

[Flags]
public enum PasswordPolicy
{
  DOMAIN_PASSWORD_COMPLEX=1,
  DOMAIN_PASSWORD_NO_ANON_CHANGE=2, 
  DOMAIN_PASSWORD_NO_CLEAR_CHANGE=4, 
  DOMAIN_LOCKOUT_ADMINS=8,
  DOMAIN_PASSWORD_STORE_CLEARTEXT=16,
  DOMAIN_REFUSE_PASSWORD_CHANGE=32
}
 
public class DomainPolicy
{
  ResultPropertyCollection attribs;
 
  public DomainPolicy(DirectoryEntry domainRoot)
  {
    string[] policyAttributes = new string[] {
      "maxPwdAge", "minPwdAge", "minPwdLength", 
      "lockoutDuration", "lockOutObservationWindow", 
      "lockoutThreshold", "pwdProperties", 
      "pwdHistoryLength", "objectClass", 
      "distinguishedName"
      };
 
    //we take advantage of the marshaling with
    //DirectorySearcher for LargeInteger values...
    DirectorySearcher ds = new DirectorySearcher(
      domainRoot,
      "(objectClass=domainDNS)",
      policyAttributes,
      SearchScope.Base
      );
 
    SearchResult result = ds.FindOne();
 
    //do some quick validation...							  
    if (result == null)
    {
      throw new ArgumentException(
        "domainRoot is not a domainDNS object."
        );
    }
 
    this.attribs = result.Properties;
  }
 
  //for some odd reason, the intervals are all stored
  //as negative numbers. We use this to "invert" them
  private long GetAbsValue(object longInt)
  {
    return Math.Abs((long)longInt);
  }
 
  public TimeSpan MaxPasswordAge
  {
    get
    {
      string val = "maxPwdAge";
      if (this.attribs.Contains(val))
      {
        long ticks = GetAbsValue(
          this.attribs[val][0]
          );
 
        if (ticks > 0)
          return TimeSpan.FromTicks(ticks);
      }
 
      return TimeSpan.MaxValue;
    }
  }
 
  public PasswordPolicy PasswordProperties
  {
    get
    {
      string val = "pwdProperties";
      //this should fail if not found
      return (PasswordPolicy)this.attribs[val][0];
    }
  }
  
  //truncated for book space
}

Listing 10.7 is meant to run on an Active Directory domain. Where does this leave ADAM instances? By default, ADAM will assume any local or domain policies on the Windows 2003 server where it is running. This means that if our Windows 2003 server is a member of the domain, we can simply use code similar to that in Listing 10.7. If, however, the server is running in a workgroup configuration, the policy will be determined locally. Therefore, Listing 10.7 would not be appropriate. Instead, we would need to know our local policy or attempt to discover it using Windows Management Instrumentation (WMI) classes.

Determining Password Expiration

Earlier in this chapter, we mentioned that accounts could have passwords that expire. Most Active Directory domains and many ADAM instances force passwords to expire periodically to improve security. As such, we often need to know when a user’s password will expire.

Determining password expiration on user accounts in Active Directory and ADAM might appear tricky, but really, it is a simple matter of calculation. Password expiration is determined based on when an individual password was last changed, and on the domain-wide password expiration policy, which we detailed in the previous section. The algorithm is essentially this:

if "password change date" + "max password age" 
  >= "now"   "password is expired"

Typically, Windows monitors password expiration and will inform a user that her password is expiring soon when she logs on locally to Windows. It then provides a mechanism to change the password. As long as the user changes her password before it expires, she can continue to log in to the domain and all is good. However, if the password expires, then the user cannot log in again until an administrator resets it.

This situation is not as straightforward for ADAM users, as there is no natural “login” process that informs users of pending password expiration and prompts them for a password change. Instead, it is completely up to the developer to supply both a notification and a means by which to change a password when using ADAM.

Programmatic LDAP binds to either directory must be handled explicitly by the developer, as we will not be warned of pending password expiration. Once a password has expired, all LDAP binds will fail until the password is reset by the user or an administrator.

 

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.

 

Searching for Accounts with Expiring Passwords

Another thing we may wish to do is find all of the accounts with passwords expiring within a certain time range, perhaps to send an email notification directing users to a web-based portal where they can change their passwords. This is important for ADAM users and any Active Directory users that do not typically log in to Windows via the workstation.

The crux of this search is based on creating a search filter with the correct values. Let’s say we want to find user accounts with passwords expiring between two dates. Since password expiration is based on the date the password was last changed and the maximum password age domain policy, we subtract the maximum password age from the two dates to get the values of pwdLastSet that will match. The code might look like that shown in Listing 10.12.

Listing 10.12: Finding Expiring Passwords

public static string GetExpirationFilter(
  DateTime startDate,
  DateTime endDate,
  TimeSpan maxPwdAge
  )
{    
  Int64 lowDate;
  Int64 highDate;
  string filterPattern 
    = "(&(sAMAccountType=805306368)"
    + "(pwdLastSet>={0})(pwdLastSet<={1}))"
  
  lowDate = startDate.Subtract(maxPwdAge).ToFileTime();
  highDate = endDate.Subtract(maxPwdAge).ToFileTime();
      
  return String.Format( 
    filterPattern, 
    lowDate, 
    highDate 
    );
}

A complete sample that enumerates users with expiring passwords between two dates is available on this book’s web site.

In both examples, we see that .NET makes this especially easy. The built-in support for dates, time spans, and Windows FILETIME structures simplifies much of the work. We can also easily construct variations on this, using similar techniques, to find accounts whose passwords have already expired, or to find all accounts that will expire before or after a certain date.

NOTEPerformance Implications of Using pwdLastSetThe pwdLastSet attribute is not indexed, nor is it stored in the global catalog in Active Directory by default. As such, this search cannot be performed across an entire forest, and it can be slow, introducing the possibility of timeouts. Using a small page size for our DirectorySearcher, our results will be returned more quickly and we can help mitigate some of these risks. It is definitely much faster than enumerating all of the users in the domain and looking at each individual attribute value on the client side.

 

Determining Last Logon

The last time a user has logged onto the domain is held in an attribute on the user object. Called lastLogon, this attribute is a nonreplicated attribute, which means that each domain controller holds its own copy of the attribute, likely with different values. Checking the last time a user has logged onto the domain requires us to visit each domain controller and read the attribute. The value found for the latest lastLogon is the value we are after.

We covered in Chapter 9 how to use the Locator to enumerate all the domain controllers. We will use this technique again to iterate through each controller and retrieve the lastLogon attribute for each user. Listing 10.13 demonstrates how to accurately determine the last time a user has logged into the domain.

Listing 10.13: Finding a User’s Last Logon

string username = "user1";
string domain = "mydomain.com";
 
public static void 
  LastLogon(string username, string domain)
{
  DirectoryContext context = 
    new DirectoryContext(
    DirectoryContextType.Domain,
    domain
    );
  
  DateTime latestLogon = DateTime.MinValue;
  string servername = null;
  
  DomainControllerCollection dcc = 
    DomainController.FindAll(context);
  
  foreach (DomainController dc in dcc)
  {
    DirectorySearcher ds;
 
    using (dc)
    using (ds = dc.GetDirectorySearcher())
    {
      ds.Filter = String.Format(
        "(sAMAccountName={0})",
        username
        );
      ds.PropertiesToLoad.Add("lastLogon");
      ds.SizeLimit = 1;
      
      SearchResult sr = ds.FindOne();
      
      if (sr != null)
      {
        DateTime lastLogon = DateTime.MinValue;
        if (sr.Properties.Contains("lastLogon"))
        {
          lastLogon = DateTime.FromFileTime(
            (long)sr.Properties["lastLogon"][0]
            );
        }
        
        if (DateTime.Compare(lastLogon,latestLogon) > 0)
        {
          latestLogon = lastLogon;
          servername = dc.Name;
        }
      }
    }
  }
  
  Console.WriteLine(
    "Last Logon: {0} at {1}",
    servername,
    latestLogon.ToString()
    );
}

We are using the SDS.AD namespace here to enumerate all of our domain controllers, and then we are using DirectorySearcher and SearchResult to marshal the LargeInteger syntax lastLogon attribute more easily. In domains with widely distributed domain controllers, we should be aware that network latency can slow this technique dramatically.

We should further keep in mind that this technique is relatively slow because it must bind to each domain controller in turn to find the user account and retrieve the lastLogon value. However, it is accurate, as each domain controller is searched and the latest logon is found.

 

Finding Stale Accounts

There is one more method we can use for finding old or unused accounts if we’re running Windows Server 2003 Active Directory in full Windows 2003 mode. The user class schema has been updated to add a new attribute called lastLogonTimestamp. This is a replicated value that is updated periodically. How often the attribute is updated depends on a new domain policy attribute named msDS-LogonTimeSyncInterval. By default, this value is 14 days, but it is configurable. As such, this attribute is not accurate for purposes of determining exactly the last time a user logged into the domain. We must use the lastLogon attribute when accuracy matters.

The syntax of this attribute is LargeInteger, so it is similar to other techniques we have already demonstrated. We can simply create a filter using the DateTime.ToFileTime method appropriately:

//find all users and computers that
//have not been used in 30 days
String filter = String.Format(
  "(&(objectClass=user)" + 
  "(lastLogonTimestamp<={0}))",
  DateTime.Now.Subtract
    (TimeSpan.FromDays(30)).ToFileTime()
  );

At first blush, this seems like a pretty easy thing to do. Indeed it is, if we can live with the following limitations.

  • It is inaccurate up to the value of msDS-LogonTimeSyncInterval (14 days by default). This means we can be no better than the resolution of this attribute in terms of our accuracy in finding accounts.
  • This is a Windows 2003 Active Directory–only feature, and even then, only when running in Windows 2003 mode.

Only NTLM and Kerberos logins update this attribute. Service Pack 1 must be applied to correct problems with NTLM as well. This means that any other type of operation that generates a login—certificates, custom Security Support Provider Interface (SSPI), Kerberos Service for User (S4U), and so on—will not update this attribute.

Additional caveats that depend on when the domain functional level was increased also affect the accuracy of this attribute.

 

Determining Account Lockout

Determining whether an account is locked out is a strange odyssey. On the surface, it appears simple enough: We have a flag on userAccountControl called UF_LOCKOUT. Surely, that would mean that checking to see if the flag was flipped would tell us whether an account was locked out, right? Well, that really depends on which provider we use. As we mentioned earlier in this chapter, the userAccountControl attribute is inaccurate in terms of determining an account’s lockout status for the LDAP provider. The situation is somewhat better if we use the WinNT provider, as this version of the userAccountControl attribute accurately reflects lockout status. Microsoft was aware of this flakey implementation and fixed it on Windows 2003 Active Directory and ADAM with the msDS-User-Account-Control-Computed attribute. This constructed attribute will accurately reflect the UF_LOCKOUT flag for the LDAP provider. Listing 10.14 shows a sample of how this would work.

//user is a DirectoryEntry for our user account
 
string attrib = 
  "msDS-User-Account-Control-Computed";
 
//this is a constructed attrib
user.RefreshCache(new string[]{attrib});
 
const int UF_LOCKOUT = 0x0010;
 
int flags = 
  (int)user.Properties[attrib].Value;
 
if (Convert.ToBoolean(flags & UF_LOCKOUT))
{
  Console.WriteLine(
    "{0} is locked out",
    user.Name
    );
}

There are a couple problems with this method, of course.

  • This works only for Windows 2003 Active Directory and ADAM, not for Windows 2000 Active Directory.
  • Since msDS-User-Account-Control-Computed is a constructed attribute, it cannot be used in an LDAP search filter.

Unfortunately, since this attribute cannot be used in a search filter, we really cannot use this method to find locked accounts proactively. Luckily, we can actually compute whether an account is locked out fairly accurately, and search for it. Previously in this chapter, we showed how we could determine the domain’s lockout duration policy. Used in conjunction with the lockoutTime attribute, we can accurately predict whether an account is locked out, and search for it. Listing 10.15 shows one such example.

Listing 10.15: Searching for Locked-Out Accounts

class Lockout : IDisposable
{
  DirectoryContext context;
  DirectoryEntry root;
  DomainPolicy policy;
 
  public Lockout(string domainName)
  {
    this.context = new DirectoryContext(
      DirectoryContextType.Domain,
      domainName
      );
 
    //get our current domain policy
    Domain domain = Domain.GetDomain(this.context);
    
    this.root = domain.GetDirectoryEntry();
    this.policy = new DomainPolicy(this.root);      
  }
 
  public void FindLockedAccounts()
  {
    //default for when accounts stay locked indefinitely
    string qry = "(lockoutTime>=1)";
 
    TimeSpan duration = this.policy.LockoutDuration;
 
    if (duration != TimeSpan.MaxValue)
    {
      DateTime lockoutThreshold =
        DateTime.Now.Subtract(duration);
 
      qry = String.Format(
        "(lockoutTime>={0})",
        lockoutThreshold.ToFileTime()
        );
    }
    
    DirectorySearcher ds = new DirectorySearcher(
      this.root,
      qry
      );
 
    using (SearchResultCollection src = ds.FindAll())
    {
      foreach (SearchResult sr in src)
      {
        long ticks =
          (long)sr.Properties["lockoutTime"][0];
 
        Console.WriteLine(
          "{0} locked out at {1}",
          sr.Properties["name"][0],
          DateTime.FromFileTime(ticks)
          );
      }
    }
  }
  
  public void Dispose()
  {
    if (this.root != null)
    {
      this.root.Dispose();
    }
  }
}

Listing 10.15 gives us a fairly simple way of finding accounts that have been locked. We are using the DomainPolicy helper class we introduced earlier in this chapter to read the lockoutDuration attribute on the root domain. If the attribute is set to TimeSpan.MaxValue, it means that an account is to stay locked until an administrator unlocks it. We are accounting for this policy possibly by setting our search filter to designate that any account with a lockoutTime with a nonzero value is locked out. This filter is appropriate only when our domain policy tells us that accounts are to be locked out indefinitely, until an administrator unlocks them.

 

Managing Passwords for Active Directory Users

Unlike most Active Directory and ADAM user-management tasks, which we perform through simple manipulation of Active Directory objects and attributes via LDAP, managing passwords is a bit complex. Password changes require very special semantics that are enforced by the server, and developers need to understand these semantics for password management applications to be successful.

In order to try to facilitate the password management process, ADSI exposes two methods on the IADsUser interface: SetPassword and ChangePassword. SetPassword is used to perform an administrative reset of a user’s password and is typically performed by an administrator. Knowledge of the previous password is not required. ChangePassword is used simply to change the password from one value to another and is typically performed only by the user represented by the directory object. It does require knowledge of the previous password, and thus it takes the old and new passwords as arguments.

Since the DirectoryEntry object does not directly expose the IADsUser ADSI interface, this is one case where we must use the DirectoryEntry.Invoke method to call these ADSI methods via late-bound reflection:

//given a DirectoryEntry "entry" 
//that points to a user object
//this will reset the user’s password
entry.Invoke("SetPassword", 
  new object[] {"newpassword"});
 
//this will change the user’s password
entry.Invoke("ChangePassword", 
  new object[] {"oldpassword", "newpassword"});

Note that the parameters to the SetPassword and ChangePassword methods are passed in as an array of objects that contain strings.

 

Password Management Complications

This seems simple enough; just call the right method and pass in the right arguments. Unfortunately, in practice this turns out not to be the case, as password management functions tend to be the most troublesome aspect of programmatic user management beyond the normal issues related to security that we discussed in Chapter 8.

Password management can be hard, for a variety of reasons.

  • Password policy and security can be complicated and must be understood.
  • The underlying ADSI methods actually try a complicated series of different approaches to set or change the password, and all of them use different protocols and have different requirements and failure modes.
  • The errors returned by ADSI are often not helpful and the resulting exception in .NET needs other special care.

Developers who have issues reliably using SetPassword and ChangePassword in all environments are unfortunately not in the minority. However, the following exploration of these three points will be immensely helpful.

 

Understanding Password Policy and Security

Active Directory enforces password policy at the domain level. Password policies that can be set include:

  • The minimum allowed length
  • The maximum allowed age
  • The minimum allowed age (how often we can change passwords)
  • Password complexity
  • Password history (determines when we can reuse an old password)

Complex passwords are just passwords that require some mix of upper- and lowercase letters, numbers, and special characters. All of these policies are exposed via attributes set in the domain root object, which can easily be found via the defaultNamingContext attribute on RootDSE. We discussed how to find these domain-wide policy settings earlier in this chapter.

Knowing what these values are will help us predict how the server will behave when asked to modify any given password. All of the policies that are enabled are enforced for each password modification; however, administrative password resets (as opposed to end-user password changes) ignore all of them except for length and complexity. This makes sense, since an administrator who (presumably) has no knowledge of the user’s previous passwords performs the reset.

Password security is relatively straightforward. Essentially, administrative users have rights to reset any account’s password, and normal users have rights to change only their own passwords. The only trick here is that the right to reset passwords can be delegated beyond the scope of the default groups, such as Domain Admins and Account Operators, so it is possible that users outside of those groups have this right.

From the application developer’s perspective, our job is to know which of the two tasks we are trying to perform and to make sure that we have the correct credentials to do so. This is pretty easy when building applications to change passwords, as the user has to give us her old password to make the change, so it is relatively easy to bind to the directory with her credentials directly and avoid any tricky impersonation scenarios (see Chapter 8). For password reset operations, our options can equally vary between using the current user’s security context (if it’s an admin application), to using a trusted service account (this is often done with helpdesk-type applications). There is not a clear-cut answer of which method to use here, and it will often depend on the type of application we are trying to build. Chapter 8 explains these scenarios in detail, especially for web applications.

 

Understanding the Underlying ADSI Methods

The SetPassword and ChangePassword methods may seem simple on the outside, but under the hood, it is just the opposite. There is a variety of different ways to modify passwords against Active Directory, and these methods pretty much try all of them. On the one hand, this is great for developers, as this approach gives developers the best possible chance that their operation will succeed. The downside of this approach is that each technique has different requirements and different ways to fail, so figuring out why things are not working can be especially difficult.

SetPassword and ChangePassword attempt password modifications using the following methods, in order:

  • LDAP password modification over an SSL channel
  • The Kerberos set password protocol over the Kerberos password port (SetPassword only)
  • A Net* API remote procedure call (RPC)

As noted in the list, the Kerberos set password protocol is used only for SetPassword. ChangePassword does not seem to have an equivalent Kerberos protocol implementation. Let’s take each technique in turn.

 

LDAP Password Modification

The first technique that is always attempted is an LDAP-based password modification. The core of this technique involves modifying the unicodePwd attribute directly. SetPassword does one modification with the Replace modification type specified, and ChangePassword does two modifications with a Delete and an Add specified, in that order. Active Directory enforces a restriction that any modification to the unicodePwd attribute must be made over an encrypted channel with a cipher strength of 128 bits. Otherwise, the server will reject the attempted modification. This helps ensure that the plaintext password is not intercepted on the network.

This is where SSL comes in. If we recall from Chapter 3, Active Directory supports two mechanisms for channel encryption: SSL and Kerberos. However, only SSL supports the minimum 128-bit cipher strength on all Active Directory platforms. Kerberos-based encryption has been strengthened to meet this requirement on Windows Server 2003, but not on Windows 2000 Server. Because the function attempts to work with either version of Active Directory, it always selects only SSL for the channel encryption technique.

This is unfortunate, because Kerberos-based encryption works out of the box with Active Directory, but SSL requires additional configuration steps including the acquisition of proper SSL certificates for each participating domain controller. Since SSL/LDAP is not required for normal Active Directory operation, many administrators do not bother to configure it. As a result, SSL is often not available for performing password modifications with SetPassword and ChangePassword.

Additionally, for an SSL/LDAP bind to succeed, proper DNS names must be used to connect to the domain controller. Typically, the server authentication aspect of the SSL handshake requires that the name used to access the server match the name of the server in the certificate, which is typically a DNS name. Therefore, it is important to remember to avoid using IP addresses and NetBIOS names when accessing a domain controller if SSL support is required.

A final thing to remember is that SSL/LDAP uses TCP port 636, not port 389, like typical LDAP traffic does. We must take this into account with any network firewall restrictions to make sure the proper ports are open.

Kerberos Set Password Protocol

The Kerberos specification includes a facility for setting user passwords. This facility does not use the original user’s password, so it is used only for administrative resets (SetPassword) and not for end-user password changes using ChangePassword.

The Kerberos set password protocol also uses a different network port (TCP 441) than normal Kerberos traffic, which goes over port 88 UDP and TCP. Once again, we must consider potential firewall issues if we wish to use this protocol.

Kerberos set password is available when a secure bind was requested and the secure bind negotiates to Kerberos rather than to NTLM. Chapter 8 contains more information about this, including some sample code we can use to verify secure bind status on the server side.

Net* API RPCs

The final method attempted by the password modification methods are one of two RPC functions from the Net* family: NetUserSetInfo and NetUserChangePassword. They are used for SetPassword and ChangePassword, respectively.

The key thing to remember with these RPC functions is that they must use the current thread’s security context. They do not directly support plaintext credentials to establish a security context, as LDAP does. With Windows 2000, this had the surprising effect of ignoring any credentials supplied to DirectoryEntry for SetPassword privileges. Developers found that if they switched environmental factors, identical code that previously worked would fail. There was also no way to know which of the three methods was being selected. So, if the test environment had SSL certificates installed correctly, SetPassword and ChangePassword would work just fine. Moving the identical code to production, where perhaps the SSL certificate was not installed correctly, would fail because unbeknownst to the developer, SetPassword had actually used NetUserSetInfo under the covers. This violated most developers’ idea of obvious behavior. After all, if the previous two methods can use the credentials supplied to DirectoryEntry to affect password modifications with identical code, why wouldn’t this one? Microsoft decided to fix this unexpected behavior with Windows 2003. Now, if the code specifies plaintext credentials in DirectoryEntry, these methods will use another API, LogonUser, to create a Windows login token for the code to impersonate while making the RPC function call. This has the effect of keeping the outward behavior of all three methods the same.

For Windows 2000 users, the only way to get this third method working correctly is to make sure our current thread’s credentials are those of an account that has the proper privileges. From Chapter 8, we know this means that either our code must run under a trusted subsystem that holds these rights, or we must impersonate an account that holds these rights for the duration of the call. Please refer back to Chapter 8 for more details, and for additional references.

Finally, remember that a Windows RPC requires TCP port 135 (and potentially, other ports) to be open to the domain controller.

Error Handling with the Invoke Method in .NET

So far, all of the issues we have discussed apply to any API that calls SetPassword or ChangePassword. While error handling for these two methods is certainly relevant, it is a larger .NET topic that affects any code that uses reflection via the Invoke method. When the Invoke method is used to call any ADSI interface method, any exception thrown by the target method, including a COM error triggered via COM interop, will be wrapped in a System.Reflection.TargetInvocationException exception, with the actual exception in the InnerException property. Therefore, we will need to use a pattern like this with the Invoke method:

//given a DirectoryEntry "entry" that points
//to a user object
//this will reset the user’s password
try
{
  entry.Invoke("SetPassword", new object[]
{"newpassword"});
}
catch (TargetInvocationException ex)
{
  throw ex.InnerException;
}

Obviously, we may wish to do something different from simply rethrowing the InnerException property. The point here is that InnerException contains the information we are interested in.

The other issue here is that the exceptions coming back from ADSI vary, from the vaguely helpful to the truly bewildering. We will not attempt to list all of them here, but here are a few hints.

  • System.UnauthorizedAccessException always indicates a permissions problem.
  • A System.Runtime.InteropServices.COMException exception with ErrorCode 0x80072035 "Unwilling to perform" generally means that an LDAP password modification failed due to a password policy issue.
  • A System.Runtime.InteropServices.COMException exception from one of the Net*APIs is usually pretty specific about what the problem was.

Recommendations for Successful Password Modification Operations

In our experience, it is possible to get SetPassword and ChangePassword to work in just about any environment, as long as we understand what they are doing under the hood and we recognize any dependencies involved in each technique. However, the approach that yields the most consistent results is to enable SSL on our domain controllers so that LDAP password modifications will be used.

Administrators may complain that they do not want to enable SSL due to the potential expense and complexity of obtaining third-party certificates or configuring an internal Windows certificate authority. Given that this is not required for general-purpose Active Directory operation, they may regard this as a nice-to-have feature and try to insist that you find another way to get this to work. We suggest you do your best to try to convince them otherwise!

 

Why Can’t We Do LDAP Password Modifications Directly in SDS?

You may be wondering why we cannot use SDS to modify the unicodePwd attribute directly. After all, it seems like this should be possible. However, even though we can create the correct value syntax to set unicodePwd correctly, the ADSI property cache prevents this from working. The fact that unicodePwd is a “write-only” attribute seems to interfere with our ability to modify it. It may be possible to reset passwords (depending on the version of the .NET Framework we are using), but it is definitely not possible to change passwords.

 

SDS.P to the Rescue

This is one area where we can do something with System.DirectoryServices.Protocols (SDS.P) that we cannot do with SDS. The lower-level access to direct LDAP modification operations available in SDS.P allows us to perform LDAP password modifications. There are three keys to this approach.

  • A 128-bit encrypted channel must be established, either by SSL or by secure bind channel encryption.
  • The unicodePwd attribute value is actually submitted as an octet string (byte array) that contains the Unicode encoding of the password value surrounded by double quotes (see Listing 10.16).
  • The reset password takes a single modification operation with the Replace operation type, and the change password operation takes a Delete operation of the old value followed by an Add of the new value.

One interesting thing here is the secure channel requirement. If we have both client and server with operating systems more recent than Windows 2000, we can use the built-in Kerberos-based channel encryption that is available with a typical secure bind and we do not need SSL. This gives us some options that are not available with the ADSI methods.

Listing 10.16 demonstrates how we might apply SDS.P to password modification operations. It is implemented to allow both password changes and resets and it works with either type of channel encryption, although only one set of options is demonstrated.

Listing 10.16: Using .Protocols for Password Ops

using System; using System.DirectoryServices.Protocols; using System.Net; using System.Text; public class PasswordModifier { public static void Main() { NetworkCredential credential = new NetworkCredential( "someuser", "Password1", "domain" ); DirectoryConnection connection; try { //change these options to use Kerberos encryption connection = GetConnection( "domain.com:636", credential, true ); ChangePassword( connection, "CN=someuser,CN=users,DC=domain,DC=com", "Password1", "Password2" ); Console.WriteLine("Password modified!"); IDisposable disposable = connection as IDisposable; if (disposable != null) disposable.Dispose(); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } private static DirectoryConnection GetConnection( string server, NetworkCredential credential, bool useSsl ) { LdapConnection connection = new LdapConnection(server); if (useSsl) { connection.SessionOptions.SecureSocketLayer = true; } else { connection.SessionOptions.Sealing = true; } connection.Bind(credential); return connection; } private static void ChangePassword( DirectoryConnection connection, string userDN, string oldPassword, string newPassword ) { DirectoryAttributeModification deleteMod = new DirectoryAttributeModification(); deleteMod.Name = "unicodePwd"; deleteMod.Add(GetPasswordData(oldPassword)); deleteMod.Operation= DirectoryAttributeOperation.Delete; DirectoryAttributeModification addMod = new DirectoryAttributeModification(); addMod.Name = "unicodePwd"; addMod.Add(GetPasswordData(newPassword)); addMod.Operation = DirectoryAttributeOperation.Add; ModifyRequest request = new ModifyRequest( userDN, deleteMod, addMod ); DirectoryResponse response = connection.SendRequest(request); } private static void SetPassword( DirectoryConnection connection, string userDN, string password ) { DirectoryAttributeModification pwdMod = new DirectoryAttributeModification(); pwdMod.Name = "unicodePwd"; pwdMod.Add(GetPasswordData(password)); pwdMod.Operation = DirectoryAttributeOperation.Replace; ModifyRequest request = new ModifyRequest( userDN, pwdMod ); DirectoryResponse response = connection.SendRequest(request); } private static byte[] GetPasswordData(string password) { string formattedPassword; formattedPassword = String.Format("\"{0}\"", password); return (Encoding.Unicode.GetBytes(formattedPassword)); } }

About thangletoan

Hallo Aloha

Posted on 12/05/2012, in Active Directory AD, AD developer, Công nghệ và Giáo dục, Chính sách CNTT. Bookmark the permalink. Để lại bình luận.

Gửi phản hồi

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Log Out / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Log Out / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Log Out / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Log Out / Thay đổi )

Connecting to %s

%d bloggers like this: