Sometimes your .NET applications need to interact with Microsoft Active Directory (AD) to authenticate users, get a list of users, retrieve groups, or determine which users are within which AD groups. There are a few different approaches you can use to retrieve information from your AD database within your domain.
One approach is to utilize the Lightweight Directory Access Protocol (LDAP) using the DirectoryEntry
and DirectorySearch
classes under the System.DirectoryServices
namespace. Another approach is to use the complete set of class wrappers around AD under the System.DirectoryServices.AccountManagement
namespace.
In this article, you will learn to use LDAP queries to retrieve information from your AD database. The LDAP classes are much faster and allow you to get at almost all of AD, whereas the wrapper classes only allow you to get at Users, Groups, and Computer objects in AD. You will find that the DirectoryEntry
and DirectorySearcher
objects are faster than the objects in the System.DirectoryServices.AccountManagement
namespace.
A few definitions are in order before we get into the actual code. First off, AD is a database-based system that provides authentication, directory, policy, and other services in a Microsoft Windows environment. LDAP is a language for querying and modifying items within a directory service like AD database. It is important to note that LDAP is a standard language used to query any kind of directory service. AD is a Microsoft proprietary implementation of a directory service and, as such, has some custom extensions on top of the LDAP standard language.
Building the LDAP Connection String
The first thing you must do in order to connect to any directory service is to create an LDAP connection string. A connection string uses the following format:
LDAP://DC=|SERVER NAME|[,DC=|EXTENSION|]
The connection string for a domain named XYZ.NET
looks like the following:
LDAP://DC=XYZ,DC=net
Instead of having to know your actual domain name, you can use the following generic code to query the LDAP server for the connection string.
private string GetCurrentDomainPath()
{
DirectoryEntry de = new DirectoryEntry("LDAP://RootDSE");
return "LDAP://" + de.Properties["defaultNamingContext"][0].ToString();
}
That code returns the following:
LDAP://DC=pdsa,DC=net
Get All Users
One of the first things you might wish to do is to retrieve all users from your AD. This is accomplished with a few different classes located within the System.DirectoryServices.dll
and in the System.DirectoryServices
namespace. The DirectoryEntry
class is used to hold the LDAP connection string. The DirectorySearcher
class is used to perform a search against the LDAP connection. You set the Filter
property on the DirectorySearcher
object to a valid LDAP query. Calling the FindAll()
method on the DirectorySearcher
object returns a SearchResultCollection
object. This collection of SearchResult
objects contains the values retrieved from the AD.
Listing 1 shows the complete code you need to retrieve all users from your AD domain. By default, the only property returned from your AD database is the name
property. If you are familiar with AD, you know that users can be created with email address, first name, middle name, last name, and many other properties. There are additional steps that you must perform if you wish to retrieve these properties that will be discussed in the next section.
Listing 1: With just a few lines of code, you can retrieve all users within your Active Directory.
private void GetAllUsers()
{
SearchResultCollection results;
DirectorySearcher ds = null;
DirectoryEntry de = new
DirectoryEntry(GetCurrentDomainPath());
ds = new DirectorySearcher(de);
ds.Filter = "(&(objectCategory=User)(objectClass=person))";
results = ds.FindAll();
foreach (SearchResult sr in results)
{
// Using the index zero (0) is required!
Debug.WriteLine(sr.Properties["name"][0].ToString());
}
}
What is interesting in Listing 1 is that you reference the name
property using [0]
. You would think that there can only be one name, and you are correct. However, because SearchResult
is a generic object that could contain any type of AD object, each of the properties could have more than one value. For instance, if you retrieve a Group
object from AD, one of the properties members
contains an array of member names that make up that group. Each property you retrieve needs to use the index of 0
, or if that property is a group, you can loop through that property's array by incrementing the index number until you reach the end of the array.
Retrieve Additional User Info
To keep things lightweight, LDAP only retrieves the most basic of information, such as the name of a User, Group, Organization Unit, etc. You may request more information, but you must tell LDAP what you wish to retrieve prior to performing your search. In the DirectorySearcher
object this is accomplished by adding property names to the PropertiesToLoad
property. You must know the names of the properties that are available in your directory service. Listing 2 shows adding the appropriate properties for Microsoft's AD database to retrieve information such as the user's first name (givenname
), last name (sn
), email address (mail
), and login name (userPrincipalName
).
Listing 2: Add property names to the PropertiesToLoad property to retrieve those values from your directory service
private void GetAdditionalUserInfo()
{
SearchResultCollection results;
DirectorySearcher ds = null;
DirectoryEntry de = new DirectoryEntry(GetCurrentDomainPath());
ds = new DirectorySearcher(de);
// Full Name
ds.PropertiesToLoad.Add("name");
// Email Address
ds.PropertiesToLoad.Add("mail");
// First Name
ds.PropertiesToLoad.Add("givenname");
// Last Name (Surname)
ds.PropertiesToLoad.Add("sn");
// Login Name
ds.PropertiesToLoad.Add("userPrincipalName");
// Distinguished Name
ds.PropertiesToLoad.Add("distinguishedName");
ds.Filter = "(&(objectCategory=User)(objectClass=person))";
results = ds.FindAll();
foreach (SearchResult sr in results)
{
if (sr.Properties["name"].Count > 0)
Debug.WriteLine(sr.Properties["name"][0].ToString());
// If not filled in, then you will get an error
if (sr.Properties["mail"].Count > 0)
Debug.WriteLine(sr.Properties["mail"][0].ToString());
if (sr.Properties["givenname"].Count > 0)
Debug.WriteLine(sr.Properties["givenname"][0].ToString());
if (sr.Properties["sn"].Count > 0)
Debug.WriteLine(sr.Properties["sn"][0].ToString());
if (sr.Properties["userPrincipalName"].Count > 0)
Debug.WriteLine(sr.Properties["userPrincipalName"][0].ToString());
if (sr.Properties["distinguishedName"].Count > 0)
Debug.WriteLine(sr.Properties["distinguishedName"][0].ToString());
}
}
An additional property you might find useful is distinguishedName
. This gives you the full LDAP query for that particular user. This LDAP query looks similar to this.
CN=Person,CN=Bruce Jones,DC=XZY,DC=net
Notice the “if” statement in the code in Listing 2 prior to displaying any of the properties. The reason for this is that these properties are optional within AD and if you don't perform the check, you could get a null
reference exception. You don't need to put an if
statement around the name and distinguishedName
properties because these will always be there, but you might want to keep things consistent.
Build a UserSearcher Method
As you can imagine, you will probably create a DirectorySearcher
for retrieving users in many places. It's a good idea to create a method that creates the DirectorySearcher
object for you and populates it with the list of properties that you are interested in. Listing 3 shows a method called BuildUserSearcher
to which you will pass in a DirectoryEntry
object. An instance of a DirectorySearcher
object is created, and then the properties are added to the PropertiesToLoad
property.
Listing 3: Create a generic method that you can call over and over to load those properties you wish to retrieve for an AD user.
private DirectorySearcher BuildUserSearcher(DirectoryEntry de)
{
DirectorySearcher ds = null;
ds = new DirectorySearcher(de);
// Full Name
ds.PropertiesToLoad.Add("name");
// Email Address
ds.PropertiesToLoad.Add("mail");
// First Name
ds.PropertiesToLoad.Add("givenname");
// Last Name (Surname)
ds.PropertiesToLoad.Add("sn");
// Login Name
ds.PropertiesToLoad.Add("userPrincipalName");
// Distinguished Name
ds.PropertiesToLoad.Add("distinguishedName");
return ds;
}
Build Extension Method for Reading Properties
Another tedious task in the code shown in Listing 2 is constantly having to check for the existence of a property prior to retrieving it. Instead of writing this code over and over, create an extension method that either returns the property value or an empty string if the property is not found. The next code snippet is an extension method for the SearchResult
class.
public static class ADExtensionMethods
{
public static string GetPropertyValue(this SearchResult sr, string propertyName)
{
string ret = string.Empty;
if (sr.Properties[propertyName].Count > 0)
ret = sr.Properties[propertyName][0].ToString();
return ret;
}
}
The extension method can replace the code with all of the “if” statements to look like the following:
foreach (SearchResult sr in results)
{
Debug.WriteLine(sr.GetPropertyValue("name"));
Debug.WriteLine(sr.GetPropertyValue("mail"));
Debug.WriteLine(sr.GetPropertyValue("givenname"));
Debug.WriteLine(sr.GetPropertyValue("sn"));
Debug.WriteLine(sr.GetPropertyValue("userPrincipalName"));
Debug.WriteLine(sr.GetPropertyValue("distinguishedName"));
}
Searching for Users
Instead of getting all users, you might wish to retrieve just a subset of users. This can be accomplished quite easily. as shown in Listing 4. You only need to add one additional LDAP query to the Filter
property. Adding (name=P*
) searches for all users with a name that begins with the letter P.
ds.Filter = "(&(objectCategory=User)(objectClass=person)(name=" + userName + "*))";
In Listing 4, you can see an example method to which you will pass a complete or partial user name. You build a DirectorySearcher
object and set the filter as described in the code snippet above. The rest of the code is the same as presented in Listing 2 except you are now using the BuildUserSearcher
method and the extension method to retrieve a property of the user.
Listing 4: Add an additional LDAP query to the Filter property to perform a fuzzy search for users.
private void SearchForUsers(string userName)
{
SearchResultCollection results;
DirectorySearcher ds = null;
DirectoryEntry de = new DirectoryEntry(GetCurrentDomainPath());
// Build User Searcher
ds = BuildUserSearcher(de);
ds.Filter = "(&(objectCategory=User)(objectClass=person)(name=" + userName + "*))";
results = ds.FindAll();
foreach (SearchResult sr in results)
{
Debug.WriteLine(sr.GetPropertyValue("name"));
Debug.WriteLine(sr.GetPropertyValue("mail"));
Debug.WriteLine(sr.GetPropertyValue("givenname"));
Debug.WriteLine(sr.GetPropertyValue("sn"));
Debug.WriteLine(sr.GetPropertyValue("userPrincipalName"));
Debug.WriteLine(sr.GetPropertyValue("distinguishedName"));
}
}
Get One User
You can search for a specific user by using the previous technique of adding an LDAP query. Just eliminate the asterisk (*
) from the query in order to do an exact match. Notice that the code in Listing 5 uses a SearchResult
instead of a SearchResultCollection
. Call the FindOne
method instead of FindAll
because you are interested in retrieving a single user and not a list. If the user is not found, a null SearchResult
object is returned.
Listing 5: Pass in a single user name and call the FindOne method to retrieve a single user from AD.
private void GetAUser(string userName)
{
DirectorySearcher ds = null;
DirectoryEntry de = new DirectoryEntry(GetCurrentDomainPath());
SearchResult sr;
// Build User Searcher
ds = BuildUserSearcher(de);
// Set the filter to look for a specific user
ds.Filter = "(&(objectCategory=User)(objectClass=person)(name=" + userName + "))";
sr = ds.FindOne();
if (sr != null)
{
Debug.WriteLine(sr.GetPropertyValue("name"));
Debug.WriteLine(sr.GetPropertyValue("mail"));
Debug.WriteLine(sr.GetPropertyValue("givenname"));
Debug.WriteLine(sr.GetPropertyValue("sn"));
Debug.WriteLine(sr.GetPropertyValue("userPrincipalName"));
Debug.WriteLine(sr.GetPropertyValue("distinguishedName"));
}
}
Get All Groups
As you have seen in all of the previous code, you are always using the same set of classes for all your querying needs. Namely; DirectoryEntry
, DirectorySearcher
, SearchResult
, and SearchResultCollection
. These classes handle almost all of your querying needs. Look at the code in Listing 6 to see an example of retrieving all Groups from Active Directory. You will notice that the code is almost identical, except the Filter
property has a different LDAP query. I added another new option to this code and that is the ability to set a Sort option. You will most likely wish to retrieve your results in some sorted order. You can specify any property to sort the data by creating a SortOption
object passing in the name of the property to sort upon.
Listing 6: Retrieving AD Groups is very similar to retrieving users. The difference is in the LDAP query you supply to the Filter property.
private void GetAllGroups()
{
SearchResultCollection results;
DirectorySearcher ds = null;
DirectoryEntry de = new DirectoryEntry(GetCurrentDomainPath());
ds = new DirectorySearcher(de);
// Sort by name
ds.Sort = new SortOption("name", SortDirection.Ascending);
ds.PropertiesToLoad.Add("name");
ds.PropertiesToLoad.Add("memberof");
ds.PropertiesToLoad.Add("member");
ds.Filter = "(&(objectCategory=Group))";
results = ds.FindAll();
foreach (SearchResult sr in results)
{
if (sr.Properties["name"].Count > 0)
Debug.WriteLine(sr.Properties["name"][0].ToString());
if (sr.Properties["memberof"].Count > 0)
{
Debug.WriteLine(" Member of...");
foreach (string item in sr.Properties["memberof"])
{
Debug.WriteLine(" " + item);
}
}
if (sr.Properties["member"].Count > 0)
{
Debug.WriteLine(" Members");
foreach (string item in sr.Properties["member"])
{
Debug.WriteLine(" " + item);
}
}
}
}
The code in Listing 6 shows one of the features of the SearchResult
property that I discussed earlier: the ability for a property to contain an array of additional information. A group can be a member of another group and a group can contain members. Specify whether you wish to retrieve these additional properties by adding them to the PropertiesToLoad
property just like you did for retrieving additional user properties. After performing the FindAll
method, all the data is retrieved and all you have to do is to check to see if there is data within the memberof and member arrays. Loop though the data and display all of the groups of which this group is a member and the members of this group.
Creating a Login Screen
Another possible use of these AD objects you have been learning is to authenticate a user against an AD. To accomplish this, you first build a login screen such as the one shown in Figure 1. Next you write code to validate that the domain, user name, and password are valid credentials within the Active Directory.
The AuthenticateUser Method
In order to authenticate a user against your Active Directory, you supply a valid LDAP path string to the constructor of the DirectoryEntry
class. The LDAP path string is in the format LDAP://DomainName
. You also pass in the user name and password to the constructor of the DirectoryEntry
class. Pass this DirectoryEntry
object to the constructor of a DirectorySearcher
object and call the FindOne
method. If the DirectorySearcher
object returns a SearchResult
, the credentials supplied are valid. If the credentials are not valid on the Active Directory, an exception is thrown. The code to authenticate is shown in Listing 7.
Listing 7: Authenticating a user is as simple as using an overload of the DirectoryEntry constructor and passing in the Domain, the user name, and the password.
private bool AuthenticateUser(string domainName, string userName, string password)
{
bool ret = false;
try
{
DirectoryEntry de = new DirectoryEntry("LDAP://" + domainName, userName, password);
DirectorySearcher dsearch = new DirectorySearcher(de);
SearchResult results = null;
results = dsearch.FindOne();
ret = true;
}
catch
{
ret = false;
}
return ret;
}
In the login window creates the following code under the Click
event procedure of the Login button.
private void btnLogin_Click(object sender, RoutedEventArgs e)
{
if(AuthenticateUser(txtDomain.Text, txtUserName.Text, txtPassword.Password))
DialogResult = true;
else
MessageBox.Show("Unable to Authenticate Using the Supplied Credentials");
}
Displaying the Login Screen
When you wish to authenticate a user, display the login screen shown in Figure 1 modally within your application. The snippet below is the code to display the login window (named winLogin
in the sample application).
private void DisplayLoginScreen()
{
winLogin win = new winLogin();
win.Owner = this;
win.ShowDialog();
if (win.DialogResult.HasValue && win.DialogResult.Value)
MessageBox.Show("User Logged In");
else
MessageBox.Show("User NOT Logged In");
}
Because this code is called from another window within your WPF application, you set the owner of the login screen to the current window. Call the ShowDialog
method on the login screen to have the login form displayed modally. After the user clicks on one of the two buttons, you need to check to see what the DialogResult
property was set to. The DialogResult
property is a nullable type, and thus you first need to check to see if the value has been set. After retrieving the result, you can now perform whatever code is appropriate for your application.
Summary
In this article, you learned how to query Active Directory to retrieve users, groups and even to authenticate a user. With just a few classes and some basic LDAP queries you can quickly retrieve information from your AD database. There is much more you can do with LDAP queries, such as adding, editing, and deleting information in your AD. I'll leave that up to you to explore these additional topics. You can find many blog posts and articles on the Web that discuss how to perform these actions on AD.
NOTE: You can download the complete sample code at my website. http://www.pdsa.com/downloads. Choose “PDSA Articles”, then “Code Magazine - Using Active Directory from .NET” from the drop-down list.