From authenticating application users against Active Directory, to programmatically adding users to Active Directory groups, it seems that a developer in a Microsoft supported environment is never too far away from Active Directory. At least this has been true in my experience. In recent years, there have been many times when I've needed to read or modify values in an Active Directory repository. Each time, I've found myself going back to previous projects or resorting to internet searching, in order to re-learn the steps and components involved.
Eventually, I decided to build a .NET library containing wrappers and components that would allow me to interact with Active Directory without having to remember each time how everything in the System.DirectoryServices namespace works.
This article will explain pieces of this library by walking through how to convert four commonly used Active Directory data types into data types that can be used in a .NET application. It will also explain how to:
- Retrieve a user from Active Directory using the System.DirectoryServices namespace
- Read the user's properties
- Commit any changes back to the Active Directory repository.
The source code for this article (see the Code Download link in the box to the right of the article title) contains the full .NET 2.0 solution, written in Visual Basic.NET. This solution contains two projects: A class library called SimpleTalk_ADDataWrappers which contains the four wrapper classes mentioned above and a console application called TestConsole which retrieves a user from an Active Directory repository and demonstrates how each of the four wrappers can be used. In this article, the wrapper objects and the console application are explained in detail.
The problem with values in Active Directory
At first glance, building this library seemed pretty easy, but I quickly hit my first road block. When you retrieve an object from Active Directory there are no strong typed properties or intellisense to help you get to the information you need. For example, you can't type user.displayName to get the string representing the user's display name. Once you get the user object from the repository, you would access the user's display name as user.properties("displayName") which returns an array of objects. So, not only do you have to know that there is a property on the user object called displayName but also what data type you're expecting it to return, and whether you should expect multiple values or just need to look at the first position in the array.
Having finally mastered displayName, what if you wanted to know when the user had last logged onto the network. You would start by getting the property named lastLogOn which will also return an array of objects. However, the returned data type is a COM object of type Interop.ActiveDs.IADsLargeInteger which is a large integer object with a high and low part representing a date and time. To make easy use of this data it would need to be converted into a Date object, and if you wanted to save it back to the repository it would need to be converted back again.
Aside from IADsLargeInteger, there are three more values that are returned from Active Directory that require some sort of conversion if you want to leverage them in a .NET application:
- The object GUID
- The object SID
- The user account control value.
First, let's look at how to convert the IADsLargeInteger into something that can be easily used.
The ADDateTime class in our .NET 2 library wraps the IADsLargeInteger object. Several Active Directory schema properties including accountExpires, badPasswordTime, lastLogon, lastLogoff and passwordExpirationDate return IADsLargeInteger values, all of which can be wrapped using the ADDateTime object.
The wrapper exposes the following three members.
- Sub New(ByVal ADsLargeInteger As IADsLargeInteger)
The constructor for the wrapper is straight forward and accepts the IADsLargeInteger value from the Active Directory repository.
- Public ReadOnly Property ADsLargeInteger() As IADsLargeInteger
This read-only property is also fairly straight forward. It exposes the IADsLargeInteger and is used for getting the value back from the wrapper when saving it to the repository.
- Public Property StandardDateTime() As DateTime
This property exposes the ADsLargeInteger value as a standard .NET DateTime object. The conversions to the underlying IADsLargeInteger are done when the property is invoked (both getting and setting) ensuring that the latest version of the IADsLargeInteger will be returned when reading this property, as well as when invoking the read only ADsLargeInteger property.
The code for the StandardDateTime() property is listed below:
Public Property StandardDateTime() As DateTime
Set(ByVal Value As DateTime)
Me._ADLI = Me.DateTimeToIADsLargeInteger(Value)
The private member _ADLI is the underlying IADsLargeInteger value. The two methods that this property uses are private methods of the ADDateTime class. These methods convert between IADsLargeInteger values and standard DateTime objects using calls to unmanaged code, and can be found in the source code region IADsLargeInteger CONVERSION METHODS.
The ADObjectGuid object wraps identifier byte arrays returned from Active Directory. Every schema object in Active Directory, including users and groups, is uniquely identified using a string of bytes. This value is returned by the Active Directory schema property objectGuid. Because the identifier values should not be modified, only read only properties are exposed on this class. The ADObjectGuid opbject exposes four members. Firstly, the constructor accepts the 128 bit byte array returned from the Active Directory repository.
Sub New(ByVal bytes As Byte())
Me._bytes = bytes
The read only property, bytes, returns the byte array as it was passed into the constructor.
Public ReadOnly Property bytes() As Byte()
The read only property, guid, returns the byte array in the form of a Guid.
Public ReadOnly Property guid() As Guid
Return New Guid(Me._bytes)
The read only property, splitOctetString, returns the identifier byte array as an octet string with each byte displayed as a hexadecimal representation and delimited by a '\' character. This format is required when using the System.DirectoryServices.DirectorySearcher to search for Active Directory objects by the objectGUID schema property.
Public ReadOnly Property splitOctetString() As String
Dim iterator As Integer
Dim builder As StringBuilder
Dim values() As Byte = Me._bytes
builder = _
New StringBuilder((values.GetUpperBound(0) + 1) * 2)
For iterator = 0 To values.GetUpperBound(0)
builder.Append("\" & values(iterator).ToString("x2"))
The ADObjectSid object is used to wrap the value of an Active Directory object's objectSid schema property. It is very similar to the ADObjectGuid. In fact, they both have the same constructor and all of the same properties, except ADObjectSid does not have a guid property. That's because the object's SID byte array is 224 bits instead of 128 and cannot be converted to the Guid data type. When an Active Directory object is created, it is assigned a SID value by the system. This value can subsequently be changed by the system but once it changes the system will never again reuse the old value with a different object. Old values are stored in the object's schema property, sidHistory, which returns an object array of SID byte arrays.
Like the ADObjectGuid, the splitOctetString property of the ADObjectSid can also be used in search filters when searching for objects by the object's SID value. This value is also often used when searching for objects by association as the association often references this value.
The ADUserAccountControl object wraps the value of the Active Directory schema property, userAccountControl. The value is simply an integer that represents several different common account control flags. Once you know what the flags are and their values, you only need to perform bitwise operations on the value to set the flag or see if the flag is set. The following snippet from the ADUserAccountControl class is the enumeration of the available flags with their values.
Public Enum enumUserAccountControlFlag
SCRIPT = &H1
ACCOUNT_DISABLED = &H2
HOMEDIR_REQUIRED = &H8
LOCKED_OUT = &H10
PASSWD_NOT_REQD = &H20
PASSWD_CANT_CHANGE = &H40
ENCRYPTED_TEXT_PASSWD_ALLWD = &H80
TEMP_DUPLICATE_ACCT = &H100
NORMAL_ACCOUNT = &H200
INTERDOMAIN_TRUST_ACCT = &H800
WORKSTATION_TRUST_ACCT = &H1000
SERVER_TRUST_ACCT = &H2000
PASSWD_NO_EXPIRE = &H10000
MNS_LOGON_ACCT = &H20000
SMART_CART_REQD = &H40000
TRUSTED_FOR_DELEGATION = &H80000
NOT_DELEGATED = &H100000
USE_DES_KEY_ONLY = &H200000
PREAUTH_NOT_REQD = &H400000
PASSWD_EXPIRED = &H800000
TRUSTED_TO_AUTH_FOR_DELEGATION = &H1000000
Most of the rest of the class contains public properties, one for each flag above, to set the flag or see if the flag is set. As an example, below, is the code for the LOCKED_OUT property.
Public Property accountLockedOut() As Boolean
Set(ByVal value As Boolean)
The bitwise operations actually take place in two convenience methods that can be seen used above:
- isFlagSet, which takes an enumUserAccountControlFlag and returns a Boolean value indicating whether or not the flag is set
- updateFlag which takes an enumUserAccountControlFlag and a Boolean value, true to set the flag and false to remove it.
Every other flag property of the ADUserAccountControl class is implemented this way as well.
Using the wrapper classes
The TestConsole project included with this article explains how a user can be retrieved from an Active Directory repository, and how the wrapper objects examined above can be leveraged to make useful information out of the data returned from the user's schema properties.
The TestConsole project has the following three references (beyond the default references included when the project is first created):
- System.DirectoryServices is used by the application to interact with the Active Directory repository
- Interop.ActiveDs contains many of the data types returned from Active Directory
- SimpleTalk_ADDataWrappers is the class library containing the wrapper objects described earlier in this article
The Main method of the TestConsole application starts by setting up some configuration variables that will be used during the execution.
Dim repositoryPath As String = "LDAP://yourRepositoryPathGoesHere"
Dim username As String = "usernameOfUserToQuery"
Dim filter As String = _
"(&(objectClass=user)(sAMAccountName=" & username & "))"
Dim ADUsername As String = "ADUsername"
Dim ADPassword As String = "ADPassword"
The repositoryPath string will specify the path to your Active Directory repository. The username string contains the domain username of the user you are going to query. The filter specifies the "query", if you will, that will be executed to find this user. The ADUsername and ADPassword strings specify the credentials for the user that will be used when binding to the user entry that you are searching for in the Active Directory repository.
In the typical domain environment, your domain credentials will give you access to bind to your own user object entry in the Active Directory repository. In other words, depending on your domain's security settings, you may not have access to query any other username in the repository but your own. Therefore, if you run into any problems running the example code, try setting the value of username to your domain username and the values for ADUsername and ADPassword to your domain username and password.
It's worth mentioning at this point that although rare, depending on your domain's security settings, this application may not work at all for your credentials. If you continue to experience problems after a tweaking the configuration variables a few times, you may need to contact your system administrator to gain the necessary access.
Once the configuration variables have the correct values, the application can initialize the directory service objects.
Dim repositoryRootEntry As New _
Dim directorySearcher As New _
The repositoryRootEntry will be the starting location in the search. In my case, when I instantiated the repositoryRootEntry, I passed in the path to the root of my domain's Active Directory tree and the administrator's username and password. The directorySearcher is used in executing searches against an entry, in our case the repositoryRootEntry using the specified filter.
Next, the application executes the search by invoking the directorySearcher's FindOne method to return a DirectoryServices.SearchResult object:
Dim result As DirectoryServices.SearchResult = _
The directorySearcher also exposes a FindAll method which returns a DirectoryServices.SearchResultCollection. The FindOne method is used in this case because there is only one user in the repository that has the specified username. So, if the number of return results can be expected to be only one, the FindOne method can be used, otherwise use FindAll. Also, note that the rest of the code is wrapped in a Try/Catch block, because from this point on if anything is going to go wrong it will happen once the connection to the repository is made.
If the result is not null, then the directorySearcher succeeded in finding the user, which will be returned as a DirectoryServices.DirectoryEntry.
Dim user As DirectoryServices.DirectoryEntry = result.GetDirectoryEntry
The remaining four method calls test the four wrapper methods described earlier in the article. Since they are all similar, let's look at the testADObjectGuid in detail.
' object guid
Dim value As Object = getProperty(user, "objectGUID")
If Not value Is Nothing Then
Dim wraper As New ADObjectGuid(value)
Console.WriteLine("User's Guid Identifier:" & _
ControlChars.Tab & ControlChars.Tab & _
ControlChars.Tab & wraper.guid.ToString)
Console.WriteLine("User's Split Octet Identifier:" &_
ControlChars.Tab & ControlChars.Tab & wraper.splitOctetString.ToString)
Console.WriteLine("Something is wrong - this user has no unique identifier.")
It may look like more code than necessary, but that's because the ControlChars end up bloating the Console.WriteLine lines. First, the value is retrieved using another method in the module, getProperty, which takes the user DirectoryEntry and the name of the property to retrieve, in this case, objectGUID. If getProperty returns nothing then a message is printed. Otherwise, the value object is loaded into a new ADObjectGuid wrapper object. Finally, the method writes the Guid and the split octet string representations of the identifier to the console.
The getProperty function is used to retrieve the value because it takes a few lines of code to get just one value from an entry. That's because, as mentioned earlier, when you request a property from a DirectoryEntry it returns an array of objects. All of the properties being requested in this module are only expected to return one value. So, this function extracts that value from the first index of the array returned. The code for the getProperty function is listed below.
Private Function getProperty _
(ByVal user As DirectoryServices.DirectoryEntry, _
ByVal propertyName As String) As Object
If user.Properties.Contains(propertyName) Then
Dim properties As _ DirectoryServices.PropertyValueCollection = _
Notice that before the function actually requests the value from the directoryEntry, it first checks to see if the entry contains the property. Although the schema may support a property on the entry, if the entry doesn't have a value for that property, it won't exist.
The properties that the entry has values for can be obtained by invoking DirectoryEntry.Properties.PropertyNames, which is an array of strings representing the property names of the properties that have values for the specific entry. There are actually several hundred properties available for many types of DirectoryEntry objects. A list of the properties can be obtained programmatically including information about which properties are mandatory and which are optional. However, this being outside of the scope for this article, to find out more information go to:
Updating the Active Directory entry
If you have downloaded the solution, you may have already noticed that there is one more method call commented out after the four tests. The updateUserEmailAddress method accepts the user DirectoryEntry and a new email address string. Before you uncomment this method, it may be a good idea to contact your network administrator to ensure that updating Active Directory properties won't have adverse effects on the other systems running on the network. For example, there may be a system on your network, like a spam filter or scheduled task that is expecting a certain user's email address to be a certain value. If this value is changed, this system may not be able to send out a notification to that user.
First, the method retrieves the user's email address, just as for the previous four tests, by calling the getProperty function and passing in the user and the property name mail, which returns the user's email address. Once it has the email address, it writes the current address to the console and then writes the newEmailAddress to the console. Then the user's email address is updated to the repository using the following code.
user.Properties("mail")(0) = newEmailAddress
When using the wrapper classes, the values are saved back to the repository in the same way. Simply replace the newEmailAddress value with the underlying value of the wrapper object. For example, updating the accountExpires property of a user would look like this:
user.Properties("accountExpires")(0) = _
At this point, an exception may be thrown if the ADUsername and ADPassword do not belong to a user with sufficient rights to update the entry's properties. Typically, as mentioned earlier, users have sufficient rights to bind to and read from their own entries but not enough rights to commit changes to the entry. Depending on your domain security settings you may experience varied results when invoking this method.
To make sure that the change worked, if you have access to actually view the Active Directory users and computers on your network, you will see that the email address for the given user has been updated. If you don't have access, you can run the TestConsole a second time and see what email address is written to the console before the email address is changed.
Things to keep in mind
While experimenting with this code, keep in mind that the scenarios described here may not work if the user being used to bind to the Active Directory entries does not have sufficient rights. Further, even if the user does have sufficient rights to read, the user may not have sufficient rights to commit changes. In the end, although the examples described in this article are pretty straight forward, different users may experience varying experiences based on the domain's security settings. For example, by default, administrators are the only users on the domain with sufficient rights to update a user entry. If this is the case on your domain, if you are not an administrator, the updateUserEmailAddress method will fail with an UnauthorizedAccessException error, when it calls user.commitChanges. For more information on best practices and how to modify Active Directory security settings visit:
Also, not all properties can be directly updated. For example, using the ADUserAccountControl wrapper to update the PASSWD_CANT_CHANGE flag will not actually change whether or not the user's password can be modified. In addition, depending on the state of the entry (disabled, password expired, etc.), some properties may be read only and although it may look like the changes have been committed, they have not. I've discovered some of these anomalies from my own experimentation and have tried to document the code where I have encountered these situations.
Conclusion – possible improvements or additions
I believe this code is a great foundation for anyone wanting to gain a deeper expertise on leveraging an Active Directory repository in a .NET application. However, as anyone reading this article can see, there are several areas for improvements and additions. One glaring issue that I have struggled with is the fact that under different scenarios in addition to insufficient access rights, this code may not work as expected or at all. Every domain is different and different user states require different implementations of the code described in this article. For example, as mentioned in the latter section, although some of these user states may be predictable, writing code that can detect and work through some of these situations is outside of the scope of this article.
Also, now that this article describes how to wrap different data types returned from Active Directory repositories, it may be nice to have a wrapper object for wrapping an entire Active Directory user entry or even a group entry.