Accessing Directory Services

Mark Strawmyer Presents: .NET Nuts & Bolts


Welcome to the next installment of the .NET Nuts & Bolts column. In this column, we'll explore how to access directory services from within .NET. Specifically, we'll focus on Microsoft's directory service called Active Directory. The topics covered will include how to use the Active Directory for items such as searching and providing authentication services for an application. It will involve using classes in the System.DirectoryServices namespace.

Directory Services

A directory service is a centralized data source that contains information relevant to an organization's computing environment. It provides access to network resource information without the client having to know the specifics of how or where the resource is connected. A directory service is commonly used to store information regarding computers, network devices such as printers, and information on users such as security credentials and application preferences.

Active Directory (AD)

Microsoft Active Directory, originally introduced in Windows 2000, is a directory service introduced by Microsoft to replace the aging Windows NT 4.0 domain architecture. It allows an organization to have a structure that more closely matches the company environment by allowing for a flexible hierarchy, support for a much larger number of network users and resources than what NT could allow, and a whole lot more. It serves as the central security point for controlling access to resources within the network. There are many concepts and designs involved with the AD. For this article, I'm going to assume you are already familiar with the AD object structure. If not, click here to visit Microsoft's site and learn more about the Active Directory.

Active Directory Services Interface (ADSI)

Active Directory Services Interface (ADSI) is a single interface from Microsoft that can be used to administer directory services. It abstracts the capabilities of directory services from different providers so that the same interface can be used regardless of the environment. Directory services that can be accessed through ADSI include, but are not limited to, the following:

  • Novell Netware Directory Service (NDS)
  • Netware 3 bindery
  • Windows NT 4.0 directory
  • Microsoft Active Directory
  • Microsoft directory capable server products: Exchange 5.5 and above, Internet Information Services, Microsoft Commerce Server, and more.

Click here to visit Microsoft's site and learn more about ADSI.

System.DirectoryServices Namespace

The Microsoft .NET Framework includes a System.DirectoryServices namespace contained in the System.DirectoryServices.dll that must be added as a reference in order to use it. It contains the DirectorySearcher and DirectoryEntry classes. These classes utilize the Active Directory Services Interface (ADSI) to interact with and manipulate the directory from within managed code and allow you to access any ADSI provider, including Active Directory.

Each object in a directory service is represented as a DirectoryEntry. It allows you the capability to access information about the directory item such as its name, modify its properties, or even rename or move it to another location in the directory.

The DirectorySearcher is used to execute a query against the directory service. You can search for a single object that matches or multiple matches. It returns a collection of DirectoryEntry objects that are read only.

Connecting to a Directory Service

The first step in doing anything with a directory service, much like a database, is to create a connection it. The act of connecting to a directory service is often referred to as binding. The connection string, known as a path, used to connect to the directory service is dependent upon the provider to which you are connecting. While Windows NT requires that you connect to a specific server, the Active Directory example allows you to bind to the name of the domain instead. A couple of sample paths are listed below.

  • Windows NT 4.0: Connect to the current machine
  • string path = "WinNT://" + Environment.MachineName
                             + ",computer";
  • Active Directory: Connect to a dev.codeguru.com domain
  • string path = "LDAP://CN=Users,DC=dev,DC=codeguru,DC=com";
    or
    string path = "LDAP://dev.codeguru.com";

Once the path statement is worked out for the appropriate provider, all that remains to bind to the directory is to pass it as a parameter to the constructor of a new instance of the DirectoryEntry class.

  • DirectoryEntry entry = new DirectoryEntry(path);

Authenticating

The DirectoryEntry class can be used to authenticate a user login and password against a directory service. Bind to the directory and pass the user login and password of the user you wish to authenticate. You force authentication to occur by retrieving the NativeObject property.

Active Directory Authentication Sample

string path = "LDAP://dev.codeguru.com";
DirectoryEntry entry = new DirectoryEntry( 
path,"dev.codeguru.com\\administrator", "");

try
{ 
// Bind to the native object to force authentication to happen
Object native = entry.NativeObject;
Console.WriteLine("User authenticated!");
}
catch( Exception ex )
{
throw new Exception("User not authenticated: " + ex.Message);
}

Searching the Directory

The DirectorySearcher object has two methods for searching a directory. There is the FindOne method that retrieves the first entry from the directory. There is the FindAll method that retrieves all directory entries that match the search string. By default, the results will load all of the properties for the directory entries that meet the filter criteria. For efficiency, you can restrict the properties loaded by using the PropertiesToLoad collection on the DirectorySearcher.

Active Directory Searching Sample using FindOne

The following sample code uses the FindOne method of the DirectorySearcher class to find the same user we authenticated a moment ago. We will limit the properties that are retrieved during the search.

DirectoryEntry entry = new
                       DirectoryEntry("LDAP://dev.codeguru.com");
try
{
  DirectorySearcher search = new DirectorySearcher(entry);
  search.Filter = "(SAMAccountName=administrator)";
  search.PropertiesToLoad.Add("Name");
  search.PropertiesToLoad.Add("displayName");
  SearchResult result = search.FindOne();
  if( result != null )
  {
    Console.WriteLine("User found");
    foreach( string key in result.Properties.PropertyNames )
    {
      // Each property contains a collection of its own
      // that may contain multiple values
      foreach( Object propValue in  result.Properties[key] )
      {
        Console.WriteLine(key + " = " + propValue);
      }
    }
  }
  else
  {
    Console.WriteLine("User not found");
  }
}
catch( Exception ex )
{
  throw new Exception("User not authenticated: " + ex.Message);
}
Console.ReadLine();

Active Directory Searching Sample using FindAll

The following sample uses the FindAll method of the DirectorySearcher class to retrieve and information on all of the users found in the directory. It then will iterate through the properties for each of the entries found and write them to the console. This time, we will not restrict the properties and will display them all.

DirectoryEntry entry = new
                       DirectoryEntry("LDAP://dev.codeguru.com");
DirectorySearcher search = new DirectorySearcher(entry);
search.Filter = "(objectClass=user)";

foreach(SearchResult result in search.FindAll())
{
DirectoryEntry dirEntry = result.GetDirectoryEntry();
foreach(string key in dirEntry.Properties.PropertyNames) 
{
    // Each property contains a collection of its own
    // that may contain multiple values
    foreach( object propVal in dirEntry.Properties[key] )
    {
      Console.WriteLine(key + " = " + propVal);
    }
  }
  Console.WriteLine("---------------");
}
Console.ReadLine();

Adding a User

Adding a user to the directory is not much more complicated than the searching we were performing before. When binding to the directory, we need to provide user credentials that have the authority to modify a user; otherwise, the update will fail. In addition, make sure that you have all of the attributes required by your AD instance; otherwise, you will get a message similar to "The server is unwilling to process the request" and not be able to add a user. Typically, only the cn (full name) and sAMAccountName are required.

There is an Invoke method that you can use to access properties through native ADSI. All user properties accessible through the DirectoryEntry class can also be set through the Invoke method. However, not all user properties are accessible through the DirectoryEntry class. It is a requirement that the user password is set using the Invoke method because it is not exposed through the DirectoryEntry class. The user must exist in the AD before the password can be set.

If you have more than one Domain Controller, please allow time for the changes to be replicated to all Domain Controllers before you will see the new user in the Active Directory Users and Computers MMC snap-in.

Adding a User Sample Code

try
{
string path = "LDAP://CN=Users,DC=dev,DC=codeguru,DC=com";
DirectoryEntry entry = new DirectoryEntry(path, 
"dev.codeguru.com\\administrator", "password");

// Create the user and set properties
DirectoryEntry user = entry.Children.Add("cn=Test User", "user");
user.Properties["sAMAccountName"].Add("testuser");
user.Properties["sn"].Add("User");
user.Properties["givenName"].Add("Test");
user.Properties["description"].Add("Test account added with code.");
user.CommitChanges();

// User has to be saved prior to this step
user.Invoke("SetPassword", new object[] {"mypassword1"} );

// Create a normal account and enable it - ADS_UF_NORMAL_ACCOUNT
user.Properties["userAccountControl"].Value = 0x200; 
user.CommitChanges();

}
catch( Exception exception )
{
Console.WriteLine( exception.Message );
}

Modifying a User

Modifying the properties of a user requires us to find an existing user, retrieve the directory entry for it, then make and commit the desired changes. When binding to the directory, we need to provide user credentials that have the authority to modify a user; otherwise, the update will fail.

If you have more than one Domain Controller, please allow time for the changes to be replicated to all Domain Controllers before you will see the new user in the Active Directory Users and Computers MMC snap-in.

Modifying a User Sample Code

try
{
  string path = "LDAP://CN=Users,DC=dev,DC=codeguru,DC=com";
  DirectoryEntry entry = new DirectoryEntry(path, 
  "dev.codeguru.com\\administrator", "password");
  DirectorySearcher search = new DirectorySearcher(entry);
  search.Filter = "(SAMAccountName=testuser)";
  SearchResult result = search.FindOne();
  DirectoryEntry user = result.GetDirectoryEntry();
  user.Properties["description"].Value = "New description for user";
  user.CommitChanges();
}
catch( Exception exception )
{
  Console.WriteLine( exception.Message );
}

Summary

You have now seen ways in which the DirectoryEntry and DirectorySearcher classes of the System.DirectoryServices namespace can be used to interact with directory services. We focused on the LDAP interface exposed by the Active Directory, but the examples can be made to work for different providers by changing the binding path.

Future Columns

The topic of the next column is yet to be determined. If you have something in particular that you would like to see explained here, you can reach me at mstrawmyer@crowechizek.com.

About the Author

Mark Strawmyer, MCSD, MCSE (NT4/W2K), MCDBA is a Senior Architect of .NET applications for large- and mid-size organizations. Mark is a technology leader with Crowe Chizek in Indianapolis, Indiana. He specializes in architecture, design, and development of Microsoft-based solutions. You can reach Mark at mstrawmyer@crowechizek.com.

# # #



About the Author

Mark Strawmyer

Mark Strawmyer is a Senior Architect of .NET applications for large and mid-size organizations. He specializes in architecture, design and development of Microsoft-based solutions. Mark was honored to be named a Microsoft MVP for application development with C# for the fifth year in a row. You can reach Mark at mark.strawmyer@crowehorwath.com.