Accessing Active Directory Through the .NET Framework

By Robert Chartier


The System.DirectoryServices namespace gives users access to some rudimentary user administration via ASP.NET. This article first reviews what the Active Directory (AD) is, then looks briefly at the actual System.DirectoryServices namespace itself, and finally presents the code that allows us to add, edit, and delete users.


What Is the Active Directory?




In today’s networked environment it’s crucial to be able to control access to each network device easily. A method is needed to control who has access to what device and when. This includes devices such as printers, files, and any other local network resource or item on the distributed network. AD provides the ability to do this, integrated with the operating system (OS), which means very intrinsic support at a very low level.



How Does AD Work?




AD is simply a hierarchical, object-orientated database that represents all of your network resources. At the top there’s typically the Organization (O), beneath that Organizational Units (OU) as containers, and finally objects that consist of your actual resources. This hierarchical format creates a very familiar and easy-to-administrate tree for systems administrators. For example, if you assign an OU access to a given resource, that access will also be persisted to the objects that are contained within it.




How Can We Access the AD?




Within the .NET Framework we are provided with the System.DirectoryServices namespace, which in turns uses Active Directory Services Interfaces (ADSI). If you have Microsoft Help installed with the .NET Framework Class library, you can refer to the following URL:
ms-help://MS.VSCC/MS.MSDNVS/cpref/html/frlrfSystemDirectoryServices.htm.


If not, take a look on MSDN directly: http://msdn.microsoft.com/library/en-us/cpref/html/frlrfSystemDirectoryServices.asp


ADSI is a way to interact with many different directory services providers through code, a programmatic interface. The classes in the System.DirectoryServices namespace can be used with any of the Active Directory service provider listed below:








Directory Service ProvidersPath
Windows NT version 5.0, Windows 2000, or Windows XPWinNT://path
Lightweight Directory Access Protocol (LDAP)LDAP://path
Novell NetWare Directory ServiceNDS://path
Novell Netware 3.xNWCOMPAT://path
Internet Information Services (IIS)IIS://

Figure 1.1 AD Service Providers


Within the System.DirectoryServices namespace there are two main classes: the System.DirectoryServices.DirectoryEntry and the System.DirectoryServices.DirectorySearcher classes. This article will not covering these in detail because the information can be read on MSDN using the above URLs. Also, note the DirectorySearcher class only works with the LDAP provider.


Also, when using DirectoryEntry objects, there is a schema for each object. A schema is the type of entry that object is. For example, if you had a DirectoryEntry object with a “User” schema, it would represent a user.

For this article, we will use the Windows 2000 Provider (WinNT://) and the System.DirectoryServices.DirectoryEntry class.




User Administration




This article’s example involves creating a Data Access Layer (DAL) to wrap around the System.DirectoryServices namespace to allow us to perform very rudimentary user administration tasks. A DAL is a method for abstracting the actual complexities of performing the data access. For example, if you were to write a DAL around an Access database, you could hide all work with ADO.NET within your objects, and when a developer needs to perform any database interaction, the developer simply needs to use these objects and not worry about specific implementation details. The developer never has to worry about ADO.NET (or even SQL). If you decide later to upgrade to Microsoft SQL Server, you simply need to change the DAL to work with the new database, and nothing else needs to be changed. For more information, read my introduction to N-Tier Application Architecture at .


The User Object

The first object we will create is used to represent the current state of any given User. This object abstracts the “User” DirectoryEntry class, and it resembles a more tangible User object for clarity.



namespace DSHelper {

public class DSUser {
public DSUser(System.DirectoryServices.DirectoryEntry user) {
this.domainName=user.Path;
this.Username=user.Name;
this.Password=user.Password;
try {
this.FullName=Convert.ToString(user.Invoke(“Get”, new object[] {“FullName”}));
this.Description=Convert.ToString(user.Invoke(“Get”, new object[] {“Description”}));
this.PasswordExpired=Convert.ToInt32(user.Invoke(“Get”, new object[] {“PasswordExpired”}));
this.RasPermissions=Convert.ToInt32(user.Invoke(“Get”, new object[] {“RasPermissions”}));
this.MaxStorage=Convert.ToInt32(user.Invoke(“Get”, new object[] {“MaxStorage”}));
this.PasswordAge=Convert.ToInt32(user.Invoke(“Get”, new object[] {“PasswordAge”}));
this.HomeDirectory=Convert.ToString(user.Invoke(“Get”, new object[] {“HomeDirectory”}));
this.LoginScript=Convert.ToString(user.Invoke(“Get”, new object[] {“LoginScript”}));
this.HomeDirDrive=Convert.ToString(user.Invoke(“Get”, new object[] {“HomeDirDrive”}));
this.userDirEntry=user;
}catch(Exception e) {
throw(new Exception(“Could not load user from given DirectoryEntry”));
}
}
public DSUser(string Username, string Password, string DomainName) {
domainName=DomainName;
if(domainName==”” || domainName==null) domainName=Environment.MachineName;
username=Username;
password=Password;
}

private object groups=null;
public object Groups{get{return groups;} set{groups=value;}}

//I removed the public getters and setters, and the private member variables
//download the full source for a complete listing
}
}


Figure 1.2 The User Object


Our user object has two default constructors. The first is used to initialize our user with a given DirectoryEntry object. It will attempt to use the Invoke method to “Get” the user’s properties off of the object.


The second constructor is used to create a new user. You only need to pass in the three required parameters and it will create a new DSUser object for you to use when creating a new user. Notice that no AD work is being done, thus no real user in your AD is being created yet.


The DAL


The next step is to create the DAL wrapper for AD. The next few sections of code will be a break down of the entire UserAdmin DAL. For a complete listing please see the downloadable.


We first create and initialize the variables we will need in our UserAdmin class:


#region default property initialization
//our error logging device. keeping it simple to avoid bloating sample code
System.Text.StringBuilder errorLog = new System.Text.StringBuilder();
private System.Boolean error=false;
public System.Boolean Error{get{return error;}}
public string ErrorLog{get{return errorLog.ToString();}}

//setup default properties
private string loginPath=”WinNT://”+Environment.MachineName+”,computer”;
private string domainName=null;
private string loginUsername=null;
private string loginPassword=null;
private System.DirectoryServices.AuthenticationTypes authenticationType = System.DirectoryServices.AuthenticationTypes.None;
private System.DirectoryServices.DirectoryEntry AD=null;
private System.Boolean connected=false;
#endregion


Figure 1.3 Default Property Initialization


Notice how we hardcode the LoginPath to be the WinNT provider. This would need to be changed in order for this DAL to work with any of the other AD service providers. Also notice that I simplified error handling by using a System.Text.StringBuilder object to hold the error log. In the case of an error, it will simply be appended to this, and the “error” Boolean variable will be set.


The next region of code to examine is the constructors for our class (see Figure 1.4, below).


#region .ctor’s
public UserAdmin() {
Connect();
}

/// <summary>
/// Allows you to create a UserAdmin class specifying all credentials, if needed
/// </summary>
/// <param name=”LoginUsername”></param>
/// <param name=”LoginPassword”></param>

/// <param name=”AuthenticationType”></param>
/// <param name=”DomainName”></param>
public UserAdmin(string LoginUsername, string LoginPassword,
System.DirectoryServices.AuthenticationTypes AuthenticationType, string DomainName) {
loginUsername=LoginUsername;
loginPassword=LoginPassword;
authenticationType=AuthenticationType;
if(DomainName==”” || DomainName==null) DomainName=System.Environment.UserDomainName;
domainName=DomainName;
Connect();
}

/// <summary>
/// An alternative way to get a UserAdmin class which allows you to specify an alternative LoginPath, for example LDAP, or IIS
/// </summary>

/// <param name=”LoginPath”></param>
/// <param name=”LoginUsername”></param>
/// <param name=”LoginPassword”></param>
/// <param name=”AuthenticationType”></param>

/// <param name=”DomainName”></param>
public UserAdmin(string LoginPath, string LoginUsername, string LoginPassword,
System.DirectoryServices.AuthenticationTypes AuthenticationType, string DomainName) {
loginPath=LoginPath;
loginUsername=LoginUsername;
loginPassword=LoginPassword;
authenticationType=AuthenticationType;
if(DomainName==”” || DomainName==null) DomainName=System.Environment.UserDomainName;
domainName=DomainName;
Connect();
}
#endregion


Figure 1.4 Constructors


The first constructor provided is the basic default constructor. This allows our object to be created without any given parameters. Since there are no required pieces of data needed, we allow for this. This means that the object will connect to the local WinNT provider with no special security privileges.


The second constructor provided allows us to specify the login credentials for connecting to the AD for the given domain. This is handy when you want to connect to the any domain with the given credentials.


Finally, the last constructor adds the LoginPath parameter. This allows you to override the LoginPath, which gives you the ability to choose an alternative service provider than the default.


The next region of code is what I call the “Action Methods.” There is actually quite an abundance of code so please refer to the downloadable source file while covering these methods. The methods in order are as follows:



public DSHelper.DSUser LoadUser(string username)


This method is used to take a username and find its DirectoryEntry in the current provider. If it cannot be found, it will simply return null.


public System.Boolean AddUserToGroup(string groupname, DSHelper.DSUser dsUser)


This method takes the name of an existing group and an instance of our custom DSUser class, and makes an attempt to add that user to the group provided. This is the first method where we can see impersonation being done. (Impersonation is discussed near the end of this article.) The core to this method is the line:


public System.Collections.ArrayList GetGroups()



We will use this method to get a list of all the groups in our domain for the given provider.


public System.Boolean DeleteUser(DSHelper.DSUser dsUser)


This will take the given user and completely delete it out of the Active Directory.


public System.DirectoryServices.DirectoryEntry SaveUser(DSHelper.DSUser dsUser)


No DAL would be complete without the ability to actually Save and Insert new records into our datastore. This method does both. It will first check the AD to see if the specified user exists or not. If it does, it will attempt to update it with the settings you provide to it. If the user does not exist, it will attempt to create it.

public System.Boolean Connect()


Lastly we need a way to connect to our datastore. The Connect() method is used to do just that. Notice, however it does not perform any impersonation or specify any credentials to use the AD at this point. We only provide credentials when we need too. This allows the DAL to be used in a Read-Only state when no credentials have been provided.


With that high-level overview, let’s put it all together and see just how we would use this DAL to create an ASP.NET user administration page.


ASP.NET User Admin Page


This sample project gives you the ability to enter in the username and password for the administrator user (to actual make changes you will need to supply these credentials). It also offers a a text box so you can enter in the name of any user account for the domain, and it will list its properties. You can edit and save, or delete that user entirely.


Take time now to review the project in detail. Concentrate on the portions where we interact with the DAL. Think of new ways to improve the interface, and actually create a more useable and friendly design. Also consider how you could create an interface using a console application.


Impersonation

The word “Impersonation” indicates that we can act as another person or user. Within our ASP.NET applications this means we can temporarily act as another user, instead of the ASP.NET user that is the default account that IIS uses for anonymous access. We want to be able to allow our code to impersonate another user that has greater access to the AD resource that we need to modify. This is accomplished fairly easily.



#region setup impersonation via interop
//need to import from COM via InteropServices to do the impersonation when saving the details
public const int LOGON32_LOGON_INTERACTIVE = 2;
public const int LOGON32_PROVIDER_DEFAULT = 0;
System.Security.Principal.WindowsImpersonationContext impersonationContext;

[DllImport(“advapi32.dll”, CharSet=CharSet.Auto)]public static extern int LogonUser(String lpszUserName,
String lpszDomain,String lpszPassword,int dwLogonType,int dwLogonProvider,ref IntPtr phToken);

[DllImport(“advapi32.dll”, CharSet=System.Runtime.InteropServices.CharSet.Auto, SetLastError=true)]public
extern static int DuplicateToken(IntPtr hToken, int impersonationLevel, ref IntPtr hNewToken);
#endregion


Figure 1.5 Setting Up Impersonation


First we need to import a few methods from the advapi32.dll, including a couple of helpful constants. The variable named impersonationContext will be used to hold the Windows user prior to the impersonation operation.


Now that we have set up impersonation, here is an example of how to use it:


if(impersonateValidUser(this.LoginUsername, this.DomainName, this.loginPassword)) {
//Insert your code that runs under the security context of a specific user here.
//don’t forget to undo the impersonation
undoImpersonation();
} else {
//Your impersonation failed. Therefore, include a fail-safe mechanism here.
}


Figure 1.6 Using Impersonation


We simply need to call the impersonateValidUser method with the valid username and password to impersonate that user. From then on, until undoImpersonation() is called, we will be performing all actions as the supplied username.


Authors Note:
To get impersonation working correctly under asp.net, change the processModel node in the machine.config
(c:winntmicrosoft.netframeworkv%VERSION%configmachine.config). The username attribute must be set to “System”. For more information
regarding this change please see the security documentation on MSDN.



Conclusion



These are the essentials needed to complete a fully functioning DAL for AD for any provider with the System.DirectoryServices namespace. Take this sample, and adapt it to your needs. Keep in mind that for any DAL to be really useful, it should not be limited to a single provider, including your normal database providers. You should be able to swap in a DAL that uses SQL Server to maintain the database of users and remove the AD DAL entirely.


There are a few more portions you may want to complete. This includes adding the functionality for Group administration. This would permit creating, editing, and deleting groups, and listing groups for each user, and listing the users within each group, etc. Unfortunately most of this functionality would need to be done via COM Interop (see
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpcongeneralconsiderations.asp). For more information on how to do this, refer to the More Info.txt file included in the downloadable ZIP file accompanying this article.




References



System.DirectoryServices namespace from MSDN, see
http://msdn.microsoft.com/library/en-us/cpref/html/frlrfSystemDirectoryServices.asp


Active Directory Services Interface (ADSI): Frequently Asked Questions from MSDN, see
http://msdn.microsoft.com/library/en-us/dnactdir/html/msdn_adsifaq.asp


System.Runtime.InteropServices namespace from MSDN, see
http://msdn.microsoft.com/library/en-us/cpref/html/frlrfSystemRuntimeInteropServices.asp


Security Concerns for Visual Basic .NET and Visual C# .NET Programmers (http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dv_vstechart/html/vbtchSecurityConcernsForVisualBasicNETProgrammers.asp)


VS .NET Security Model (http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vsent7/html/vxoriDistributedApplicationSecurityRecommendations.asp)

About the Author




Robert Chartier has developed IT solutions for more than nine years with a diverse background in both software and hardware development. He is internationally recognized as an innovative thinker and leading IT architect with frequent speaking engagements showcasing his expertise. He’s been an integral part of many open forums on cutting-edge technology, including the .NET Framework and Web Services. His current position as vice president of technology for Santra Technology has allowed him to focus on innovation within the Web Services market space.


He uses expertise with many Microsoft technologies, including .NET, and a strong background in Oracle, BEA Systems, Inc.’s BEA WebLogic, IBM, Java 2 Platform Enterprise Edition (J2EE), and similar technologies to support his award-winning writing. He frequently publishes to many of the leading developer and industry support Web sites and publications. He has a bachelor’s degree in Computer Information Systems.


More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read