Active Directory Management

Adding Directory Objects

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/sds/sds/create__delete__rename_and_move_objects.asp
To add new directory objects, use the Children property of the DirectoryEntry class. The Children property returns a DirectoryEntries object that exposes the Add method.

To add an object, bind to the container that the object is to be added to and, after adding the object, call CommitChanges to save the object from the cache to the directory. The following code example shows how to use the Add method to add new objects.

Try
    ' Bind to the Users container, add a new group and a new contact.
    Dim de As New DirectoryEntry("LDAP://CN=Users,DC=fabrikam,DC=com")
    Dim newGroup As DirectoryEntry = de.Children.Add("CN=Sales", "group")
    newGroup.CommitChanges()
    Dim newContact As DirectoryEntry = de.Children.Add("CN=New Contact", "contact")
    newContact.CommitChanges()
    ' Bind to the Computers container and add a new computer.
    Dim de01 As New DirectoryEntry("LDAP://CN=Computers,DC=fabrikam,DC=com")
    Dim newComputer As DirectoryEntry = de01.Children.Add("CN=New Computer", "computer")
    newComputer.CommitChanges()
Catch Exception1 As Exception
    If (True) Then
        ' If a COMException is thrown, then the following code can catch the text of the error.
        ' For more information about handling COM exceptions, see Handling Errors.
        Dim COMEx As System.Runtime.InteropServices.COMException = CType(Exception1, System.Runtime.InteropServices.COMException)
        Console.WriteLine(COMEx.ErrorCode)
    End If
End Try

After adding a new object, use the Exists method to verify an entry in the directory. This method is provided in the DirectoryEntry class. The following code example shows how to use Exists.

Dim ds As New System.DirectoryServices.DirectoryEntry()
If ds.Exists("LDAP://CN=Sales,CN=Users,DC=fabrikam,DC=com") = True Then
   Console.WriteLine("object exists")
Else
   Console.WriteLine("object does not exist")
End If

Deleting Directory Objects

To remove a directory object, the DirectoryEntries class offers the Remove method. The Remove method removes a child entry from the children collection of the current parent directory. This method takes only one argument, the entry name to be removed, and must be used only on an empty container or an object which is not used as a container for a directory tree, such as printers or computer nodes.

The following code snippet shows how to remove an object from the directory.

Dim entry As New DirectoryServices.DirectoryEntry("WinNT://DomainName")
Dim entryToRemove As DirectoryServices.DirectoryEntry
' Add code here to set entryToRemove to the entry you want to remove.
entry.Children.Remove(entryToRemove)

Deleting a Sub-Tree of Objects

To delete an entry and all of its children, call the DeleteTree method. The DeleteTree method deletes the current entry and all of its subtree entries from the hierarchy.

The following code snippet shows how to remove a tree of objects from the directory.

Dim entry As New DirectoryServices.DirectoryEntry("WinNT://DomainName")
entry.DeleteTree()

Renaming an Object

DirectoryEntry provides the Rename method for renaming objects in the directory.

The following code example shows how to use Rename to change the name of a User object.

Try
    ' Bind to the user object to modify.
    Dim child As New DirectoryEntry("LDAP://CN=My User Name,OU=Marketing,DC=fabrikam,DC=com")
    child.Rename("CN=New User Name")    ' Rename the object to Jeff Smith.
Catch Exception1 As Exception
    If (True) Then
        ' If a COMException is thrown, then the following code can capture the text of the error.
        ' For instructions about handling COM exceptions, see Handling Errors.
        Dim COMEx As System.Runtime.InteropServices.COMException = CType(Exception1, System.Runtime.InteropServices.COMException)
        Console.WriteLine(COMEx.ErrorCode)
    End If 'break;
End Try '}

Moving Directory Objects

To move objects to a new container, use the MoveTo method.

The following code example shows how to use MoveTo to move a user object.

Try
    ' Bind to the object to be moved.
    Dim NewUser As New DirectoryEntry("LDAP://CN=User Name,OU=Sales,DC=fabrikam,DC=com")
    ' Use the MoveTo property to define the new container where you wish to move the object to.
    NewUser.MoveTo(New DirectoryEntry("LDAP://OU=HR,DC=fabrikam,DC=com"))
Catch Exception1 As Exception
    Select Case Exception1.GetType().ToString()
        ' If InvalidOperationException is thrown, then the DirectoryEntry is not a container.
        Case "InvalidOperationException"
        Dim InvOpEx As New InvalidOperationException()
        ' Handle error, for example Console.WriteLine(InvOpEx.Message);
        Exit Select
        ' If a COMException is thrown, then the following code can catch the text of the error.
        ' For more information about handling COM exceptions, see Handling Errors.
        Case "System.Runtime.InteropServices.COMException"
        Dim COMEx As System.Runtime.InteropServices.COMException = CType(Exception1, System.Runtime.InteropServices.COMException)
        ' Handle error, for example Console.WriteLine(COMEx.ErrorCode);
        Exit  Select
    End Select
End Try

Property Cache Management

This topic shows how to identify properties that are currently in the property cache and how to refresh the property cache.

The PropertyNames property is used to find the names of all the properties that are presently in the property cache. The following code example shows how to use PropertyNames without refreshing the property cache.

try
{
    DirectoryEntry ent = new DirectoryEntry();    // Create an empty DirectoryEntry object.
    // Use PropertyNames to iterate through the property names that are presently in the cache.
    foreach(String propName in ent.Properties.PropertyNames)
    {
        Console.WriteLine(propName);        // Write out the property names.
    }
}
catch
{    // Handle errors.}

The following code example shows how to refresh all of the properties in the property cache.

Try
{
    DirectoryEntry ent = new DirectoryEntry();    // Create a DirectoryEntry object.
    ent.RefreshCache();    // Refresh the cache.
}
Catch
{    // Handle errors.}

The following code example shows how to refresh selected properties in the property cache using RefreshCache.

try
{
    DirectoryEntry ent = new DirectoryEntry();    // Create a DirectoryEntry object.
    ent.RefreshCache(new string[]{"objectClass","dc","whenCreated"});    // Refresh the objectClass, dc, and whenCreated properties.

    foreach(String propName in ent.Properties.PropertyNames)    // Iterate through each property name.
    {
        Console.WriteLine(propName);        // Write the property names. There should only be three.   
    }
}
catch
{    // Handle errors.}

Walkthrough: Viewing Local Directory Objects

The procedures in this topic demonstrate how to use DirectoryEntry to list the users, groups, and services on your local computer. DirectoryEntry uses Active Directory technology to do this work. Each entry created by the component contains a list of its properties.

To create the user interface

  1. On the File menu, point to New, and then click Project.
  2. In the New Project dialog box, choose Visual Basic .NET, Visual C#, or Visual J# in the left pane, then choose the Windows Application template. Name the project ActiveDirectory.
  3. From the Windows Forms tab of the Toolbox, drag a TreeView control onto Form1.
  4. Set the Name property of the TreeView control to viewPC.

    The TreeView control will be modified later in the walkthrough to contain three top-level nodes, one each for users, groups, and services. Each second-level node will represent one user, group, or service registered on your computer. Each user, group, and service will have two child nodes, one for its Active Directory path and one for its properties.

To configure a DirectoryEntry component

  1. In Solution Explorer, right-click your project and select Add Reference from the shortcut menu.
  2. Add a reference to System.DirectoryServices.dll
  3. From the Components tab of the Toolbox, drag a DirectoryEntry component instance to the designer.
  4. Set the Name property of the DirectoryEntry component to entryPC.
  5. Set the Path property of the DirectoryEntry component to WinNT://Domain/YourComputerName. Use your domain and computer name to direct the DirectoryEntry component to examine your local computer using the WinNT service provider for Active Directory. If you do not know your domain or computer name, contact your system administrator.

    Note  If you have the incorrect domain or computer name, the application will throw an exception at run time. You can enter WinNT://YourComputerName for the path, and the service provider will attempt to locate the computer.

To add the top-level nodes to the TreeView control

  1. In the designer, double-click Form1 to create the Form_Load event handler in the Code Editor.
  2. Modify the Form_Load method by adding code to create the three top-level nodes for users, groups, and services. When the application runs, the Children property of the component named entryPC contains all the entries found in Path (your computer). Each child is also a DirectoryEntry object. Because you are using the WinNT provider on a computer path, the SchemaClassName property of the child DirectoryEntry object will be one of "User", "Group", or "Service". A select or switch statement can be used to determine the entry type and add a node to the correct group.

    You will write the AddPathAndProperties method in the next step.

    Private Sub Form1_Load(ByVal sender As System.Object, _
    ByVal e As System.EventArgs) Handles MyBase.Load
       Dim users As New TreeNode("Users")
       Dim groups As New TreeNode("Groups")
       Dim services As New TreeNode("Services")
       viewPC.Nodes.AddRange(New TreeNode() {users, groups, services})
    
       Dim child As System.DirectoryServices.DirectoryEntry
       For Each child In entryPC.Children
          Dim newNode As New TreeNode(child.Name)
          Select Case child.SchemaClassName
             Case "User"
                users.Nodes.Add(newNode)
             Case "Group"
                groups.Nodes.Add(newNode)
             Case "Service"
                services.Nodes.Add(newNode)
          End Select
          AddPathAndProperties(newNode, child)
       Next
    End Sub
  3. Add the following code to the Form1 class to create the AddPathAndProperties method. The node parameter represents the TreeNode object associated with that entry. The entry parameter represents one entry in the directory: a user, a group, or a service.

    The DirectoryEntry component has a Properties property that contains a string-indexed collection of properties. The contents of this collection depend on the schema of the DirectoryEntry component. The Properties collection has a PropertyNames property, which is a collection of all the names of the properties. By stepping through this collection, you can retrieve each property. Each property also has a collection of associated values. In this method, you only retrieve the first value for each property.

    Private Sub AddPathAndProperties(ByVal node As TreeNode, _
    ByVal entry As System.DirectoryServices.DirectoryEntry)
       node.Nodes.Add(New TreeNode("Path: " & entry.Path))
       Dim propertyNode As New TreeNode("Properties")
       node.Nodes.Add(propertyNode)
    
       Dim propertyName As String
       Dim oneNode As String
       For Each propertyName In entry.Properties.PropertyNames
          Try
             oneNode = propertyName & ": " & _
                CType(entry.Properties(propertyName)(0), String)
          Catch
             oneNode = propertyName & ": " & _
                "No text representation."
          End Try
          propertyNode.Nodes.Add(New TreeNode(oneNode))
       Next
    End Sub

To run the program

  1. Press F5 to run the program.
  2. Open the nodes to examine the paths and properties. The users all share the same set of properties, because they all share the same schema. The same is true of the group and service nodes.

This walkthrough used the WinNT service provider for Active Directory. Other services are available, including the Lightweight Directory Access Protocol (LDAP), the Novell NetWare Directory Service (NDS), and the Novell Netware 3.x service (NWCOMPAT). Each provider provides a different set of objects to allow you to examine and manipulate directories.

DirectorySearcher can be used to search for entries from a root path. DirectorySearcher works with the LDAP provider.

Active Directory Authentication from ASP .NET

This topic demonstrates how an ASP.NET application can use Forms authentication to permit users to authenticate against the Active Directory using the Lightweight Directory Access Protocol (LDAP). After the user is authenticated and redirected, you can use the Application_AuthenticateRequest method of the Global.asax file to store a GenericPrincipal object in the HttpContext.User property that flows throughout the request.

To create a new ASP.NET Web application

  1. Start Microsoft Visual Studio .NET.
  2. On the File menu, point to New, and then click Project.
  3. Click Visual C# Projects under Project Types, and then click ASP.NET Web Application under Templates.
  4. In the Name box, type "FormsAuthAd" (without the quotation marks).
  5. Leave the default http://localhost in the Server box if you are using the local server, and then click OK.
  6. Right-click the References node in Solution Explorer, and then click Add Reference.
  7. On the .NET tab in the Add Reference dialog box, click System.DirectoryServices.dll, click Select, and then click OK.

To add System.DirectoryServices authentication code

  1. In Solution Explorer, right-click the project node, point to Add, and then click Add New Item.
  2. Click Class under Templates.
  3. Type "LdapAuthentication.cs" (without the quotation marks) in the Name box, and then click Open.
  4. Replace the existing code in the LdapAuthentication.cs file with the following code:
    using System;
    using System.Text;
    using System.Collections;
    using System.DirectoryServices;
    		
    namespace FormsAuth
    {	
      public class LdapAuthentication
      {
        private String _path;
        private String _filterAttribute;
    		
        public LdapAuthentication(String path)
        {
          _path = path;
        }
    				
        public bool IsAuthenticated(String domain, String username, String pwd)
        {
          String domainAndUsername = domain + @"\" + username;
          DirectoryEntry entry = new DirectoryEntry(_path, domainAndUsername, pwd);
    					
          try
          {	
            //Bind to the native AdsObject to force authentication.			
            Object obj = entry.NativeObject;
    		
            DirectorySearcher search = new DirectorySearcher(entry);
    		
            search.Filter = "(SAMAccountName=" + username + ")";
            search.PropertiesToLoad.Add("cn");
            SearchResult result = search.FindOne();
    		
            if(null == result)
            {
              return false;
            }
    		
            _path = result.Path;        //Update the new path to the user in the directory.
            _filterAttribute = (String)result.Properties["cn"][0];
          }
          catch (Exception ex)
          {        throw new Exception("Error authenticating user. " + ex.Message);    }
    		
          return true;
        }
    		
        public String GetGroups()
        {
          DirectorySearcher search = new DirectorySearcher(_path);
          search.Filter = "(cn=" + _filterAttribute + ")";
          search.PropertiesToLoad.Add("memberOf");
          StringBuilder groupNames = new StringBuilder();
    		
          try
          {
            SearchResult result = search.FindOne();
            int propertyCount = result.Properties["memberOf"].Count;
            String dn;
            int equalsIndex, commaIndex;
    						
            for(int propertyCounter = 0; propertyCounter < propertyCount; propertyCounter++)
            {
              dn = (String)result.Properties["memberOf"][propertyCounter];
    		    	   equalsIndex = dn.IndexOf("=", 1);
              commaIndex = dn.IndexOf(",", 1);
              if(-1 == equalsIndex)
              {
                return null;
              }
              groupNames.Append(dn.Substring((equalsIndex + 1), (commaIndex - equalsIndex) - 1));
              groupNames.Append("|");
            }
          }
          catch(Exception ex)
        { throw new Exception("Error obtaining group names. " + ex.Message);  }		
     return groupNames.ToString();
    }
    }
    }

In the previous procedure, the authentication code accepts a domain, a user name, a password, and a path to the tree in Active Directory. This code uses the LDAP directory provider. The code in the Logon.aspx page calls the LdapAuthentication.IsAuthenticated method and passes in the credentials that are collected from the user. Then, a DirectoryEntry object is created with the path to the directory tree, the user name, and the password. The user name must be in the "domain\username" format.

The DirectoryEntry object then tries to force the AdsObject binding by obtaining the NativeObject property. If this succeeds, the CN attribute for the user is obtained by creating a DirectorySearcher object and by filtering on the SAMAccountName. After the user is authenticated, the IsAuthenticated method returns true. To obtain a list of groups that the user belongs to, this code calls the LdapAuthentication.GetGroups method. The LdapAuthentication.GetGroups method obtains a list of security and distribution groups that the user belongs to by creating a DirectorySearcher object and by filtering according to the memberOf attribute. This method returns a list of groups that is separated by pipes (|). Notice that the LdapAuthentication.GetGroups method manipulates and truncates strings. This reduces the length of the string that is stored in the authentication cookie. If the string is not truncated, the format of each group appears as follows:

CN=...,...,DC=domain,DC=com

This can create a very long string. If the length of this string is greater than the length of the cookie, the authentication cookie may not be created. If this string may potentially exceed the length of the cookie, you may want to store the group information in the ASP.NET Cache object or in a database. Alternatively, you may want to encrypt the group information and store this information in a hidden form field.

The code in the Global.asax file provides an Application_AuthenticateRequest event handler. This event handler retrieves the authentication cookie from the Context.Request.Cookies collection, decrypts the cookie, and retrieves the list of groups that will be stored in the FormsAuthenticationTicket.UserData property. The groups appear in a pipe-separated list that is created in the Logon.aspx page. The code parses the string in a string array to create a GenericPrincipal object. After the GenericPrincipal object is created, this object is placed in the HttpContext.User property.

To write the Global.asax code

  1. In Solution Explorer, right-click Global.asax, and then click View Code.
  2. Add the following code at the top of the code-behind Global.asax.cs file:
    using System.Web.Security;
    using System.Security.Principal;
  3. Replace the existing empty event handler for the Application_AuthenticateRequest with the following code:
    void Application_AuthenticateRequest(Object sender, EventArgs e)
    {
      String cookieName = FormsAuthentication.FormsCookieName;
      HttpCookie authCookie = Context.Request.Cookies[cookieName];
    		
      if(null == authCookie)
      {
        return;    //There is no authentication cookie.
      }
      FormsAuthenticationTicket authTicket = null;
      try
      {
        authTicket = FormsAuthentication.Decrypt(authCookie.Value);
      }
      catch(Exception ex)
      {
        return;    //Write the exception to the Event Log.
      }
    if(null == authTicket)
      {
        return;		    //Cookie failed to decrypt.
      }		
      //When the ticket was created, the UserData property was assigned a pipe-delimited string of group names.
      String[] groups = authTicket.UserData.Split(new char[]{'|'});
      GenericIdentity id = new GenericIdentity(authTicket.Name, "LdapAuthentication");  //Create an Identity.
      GenericPrincipal principal = new GenericPrincipal(id, groups);  //This principal flows throughout the request.
      Context.User = principal;
    }

In this section, you configure the <forms>, the <authentication>, and the <authorization> elements in the Web.config file. With these changes, only authenticated users can access the application, and unauthenticated requests are redirected to a Logon.aspx page. You can modify this configuration to permit only certain users and groups access to the application.

To modify the Web.config file

  1. Open Web.config in Notepad.
  2. Replace the existing code with the following code:
    [XML]
    <?xml version="1.0" encoding="utf-8?>
    <configuration>
      <system.web>
        <authentication mode="Forms">
          <forms loginUrl="logon.aspx" name="adAuthCookie" timeout="10" path="/">
          </forms>
        </authentication>
        <authorization>
          <deny users="?"/>
          <allow users="*"/>
        </authorization>
        </identity impersonet="true"/>
      </system.web>
     </configuration>

Notice the following configuration element:

[XML]
<identity impersonate="true"/>
This causes ASP.NET to impersonate the account that is configured as the anonymous account from Microsoft Internet Information Services (IIS). As a result of this configuration, all requests to this application run under the security context of the configured account. The user provides credentials to authenticate against the Active Directory, but the account that accesses the Active Directory is the configured account.

To configure IIS for anonymous authentication

  1. In IIS, click to select the Anonymous Authentication check box.
  2. Make the anonymous account for the application an account that has permission to the Active Directory.
  3. Click to clear the "Allow IIS To Control Password" check box. The default IUSR_<computername> account does not have permission to the Active Directory.

To create the Logon.aspx page

  1. In Solution Explorer, right-click the project node, point to Add, and then click Add Web Form.
  2. Type "Logon.aspx" (without the quotation marks) in the Name box, and then click Open.
  3. In Solution Explorer, right-click Logon.aspx, and then click View Designer.
  4. Click the HTML tab in the Designer.
  5. Replace the existing code with the following code:
    [XML]
    <%@ Page language="c#" AutoEventWireup="true" %>
    <%@ Import Namespace="FormsAuth" %>
    <html>
      <body>	
        <form id="Login" method="post" runat="server">
          <asp:Label ID="Label1" Runat=server >Domain:</asp:Label>
          <asp:TextBox ID="txtDomain" Runat=server ></asp:TextBox><br>    
          <asp:Label ID="Label2" Runat=server >Username:</asp:Label>
          <asp:TextBox ID=txtUsername Runat=server ></asp:TextBox><br>
          <asp:Label ID="Label3" Runat=server >Password:</asp:Label>
          <asp:TextBox ID="txtPassword" Runat=server TextMode=Password></asp:TextBox><br>
          <asp:Button ID="btnLogin" Runat=server Text="Login" OnClick="Login_Click"></asp:Button><br>
          <asp:Label ID="errorLabel" Runat=server ForeColor=#ff3300></asp:Label><br>
          <asp:CheckBox ID=chkPersist Runat=server Text="Persist Cookie" />
        </form>	
      </body>
    </html>
    <script runat=server>
    void Login_Click(Object sender, EventArgs e)
    {
      String adPath = "LDAP://DC=..,DC=.."; //Path to you LDAP directory server
      LdapAuthentication adAuth = new LdapAuthentication(adPath);
      try
      {
        if(true == adAuth.IsAuthenticated(txtDomain.Text, txtUsername.Text, txtPassword.Text))
        {
          String groups = adAuth.GetGroups();
    		
          //Create the ticket, and add the groups.
          bool isCookiePersistent = chkPersist.Checked;
          FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(1, <?xm-deletion_mark author="Administrator" time="20030530T152131+05-30" data="_"?>
          txtUsername.Text,DateTime.Now, DateTime.Now.AddMinutes(60), isCookiePersistent, groups);
    			
          String encryptedTicket = FormsAuthentication.Encrypt(authTicket);      //Encrypt the ticket.
    				
          //Create a cookie, and then add the encrypted ticket to the cookie as data.
          HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
    		
          if(true == isCookiePersistent)
          authCookie.Expires = authTicket.Expiration;
    						
          Response.Cookies.Add(authCookie);	      //Add the cookie to the outgoing cookies collection.	
    		
          Response.Redirect(FormsAuthentication.GetRedirectUrl(txtUsername.Text, false));      //You can redirect now.
        }
        else
        {
          errorLabel.Text = "Authentication did not succeed. Check user name and password.";
        }
      }
      catch(Exception ex)
      {    errorLabel.Text = "Error authenticating. " + ex.Message;  }
    }
    </script>
  6. Modify the path in the Logon.aspx page to point to your LDAP Directory server.

The Logon.aspx page is a page that collects the information from the user and call methods on the LdapAuthentication class. After the code authenticates the user and obtains a list of groups, the code creates a FormsAuthenticationTicket object, encrypts the ticket, adds the encrypted ticket to a cookie, adds the cookie to the HttpResponse.Cookies collection, and then redirects the request to the URL that was originally requested.

The WebForm1.aspx page is the page that is requested originally. When the user requests this page, the request is redirected to the Logon.aspx page. After the request is authenticated, the request is redirected to the WebForm1.aspx page.

To modify the WebForm1.aspx page

  1. In Solution Explorer, right-click WebForm1.aspx, and then click View Designer.
  2. Click the HTML tab in the Designer.
  3. Replace the existing code with the following code:
    [XML]
    <%@ Page language="c#" AutoEventWireup="true" %>
    <%@ Import Namespace="System.Security.Principal" %>
    <html>
      <body>	
        <form id="Form1" method="post" runat="server">
          <asp:Label ID="lblName" Runat=server /><br>
          <asp:Label ID="lblAuthType" Runat=server />
        </form>	
      </body>
    </html>
    <script runat=server>
    void Page_Load(Object sender, EventArgs e)
    {
      lblName.Text = "Hello " + Context.User.Identity.Name + ".";
      lblAuthType.Text = "You were authenticated using " +   Context.User.Identity.AuthenticationType + ".";
    }
    </script>
  4. Save all files, and then compile the project.
  5. Request the WebForm1.aspx page. Notice that you are redirected to Logon.aspx.
  6. Type the logon credentials, and then click Submit. When you are redirected to WebForm1.aspx, notice that your user name appears and that LdapAuthentication is the authentication type for the Context.User.AuthenticationType property.

Note  It is recommended that you use Secure Sockets Layer (SSL) encryption when you use Forms authentication. This is because the user is identified based on the authentication cookie, and SSL encryption on this application prevents anyone from compromising the authentication cookie and any other valuable information that is being transmitted.

Troubleshooting Authentication Problems on ASP Pages

If you have an application that is working from a Windows-based or command line application, but is not working when you transfer it to an ASP.NET application, the problem likely pertains to security. The System.DirectoryServices namespace uses Active Directory Services Interfaces(ADSI) to contact discrete directory services through different ADSI providers. This topic assumes that you, the application designer, want the directory to be contacted under the security context of the ASP.NET Web user. If you do not want to do that, or you do not want to perform the resolutions that are listed in this topic, you can work around these problems by passing credentials to your DirectoryServices code through the class constructor, or by using the Username and Password properties.

What is a Primary Token?

The Active Directory relies on the security mechanism of the Windows 2000 server. To access most data in Active Directory, provide credentials to the Windows 2000 server when requesting Active Directory data. The credentials you provide must be in a primary token, which just means that the IIS server has a password, not just a hash of the password, to pass to Active Directory.

If the code works when you browse to it from the development computer that is a Web server, but the code does not work when other Web clients access the pages, you may receive an error message similar to the following.

"Failed: System.Runtime.InteropServices.COMException 
(0x80005000): Unknown error (0x80005000) at System.DirectoryServices.DirectoryEntry.Bind(Boolean throwIfFail)"
"The specified directory service attribute or value does not exist" 

This error is an indication that you do not have a primary token.

How to Acquire a Primary Token

When the Web.config file is set to identity impersonate="true"/ and authentication mode="Windows", use the Anonymous account with the following settings:

To acquire a token using an Anonymous account

  1. On the ASPX page, set the security mechanism to Anonymous only.
  2. Clear the Allow IIS to control the password check box.
  3. Set the Anonymous account to be a domain user.

When Web.config and Machine.config are set as follows:

To acquire a token using configuration settings

  1. When Web.config is set to identity impersonate="false"/ and authentication mode="Windows".
  2. When Machine.config is set to processModel username=Domain\username,password=secret. If identity impersonate="false"/ in the Web.config file, the credentials of the Base process are used. When you supply a domain user and password, you enable IIS to pass a primary token to the AD.

Double-Hop Issue

The double-hop issue occurs when the ASPX page attempts to use resources located on a server that is different from the IIS server. In our case, the first "hop" is from the web browser client to the IIS ASPX page; the second hop is to Active Directory. Active Directory requires a primary token. Therefore, the IIS server must know the password for the client to pass a primary token to Active Directory. If the IIS server has a secondary token, the NTAUTHORITY\ANONYMOUS account credentials are used. This account is not a domain account and has very limited access to Active Directory.

The double-hop using a secondary token occurs, for example, when the browser client is authenticated to the IIS ASPX page by using NTLM authentication. In this example, the IIS server has a hashed version of the password as a result of using NTLM. If IIS turns around and passes the credentials to Active Directory, IIS is passing a hashed password. Active Directory cannot verify the password so it uses NTAUTHORITY\ANONYMOUS LOGON for authentication.

If your browser client uses Basic authentication to authenticate to the IIS ASPX page, the IIS server has the client password and can make a primary token to pass to Active Directory. Active Directory can verify the password and authenticate the domain user.

Troubleshoot the Double-Hop Issue

Use one of the following methods to troubleshoot the double-hop issue.

To quickly determine whether this is a permissions issue, do the following:

To check for a permissions problem

  1. Set the ASPX page security mechanism to use Basic only.
  2. Use a client to browse to the ASPX page, and then provide domain credentials when prompted.

    If this works, the double-hop issue is likely the problem.

Another good troubleshooting test for any IIS ASPX issue when you access Active Directory involves taking your ASPX code out of the IIS environment and running it as a script file on the IIS server itself. To use this method, do the following:

To test your code

  1. Log on to the IIS server as the domain account that your browser was trying to use, and then run the code.

    This test removes the IIS server from the environment and helps troubleshoot the problem.

  2. To determine whether this is a double-hop issue, turn on auditing for directory service objects. After you turn on logging, events are written to the security event log.

    If this is your issue, you can find events in the security event log that are similar to the following:

    Event Type: Success Audit
    Event Source: Security
    Event Category: Directory Service Access 
    Event ID: 565
    Date:  3/27/2002
    Time:  3:21:41 PM
    User:  NT AUTHORITY\ANONYMOUS LOGON
    Computer: TESTDC
    Description:
    Object Open:
      Object Server: DS
      Object Type: user
      Object Name: CN=Users,DC=corp,DC=com
      New Handle ID: 0
      Operation ID: {0,68019232}
      Process ID: 264
      Primary User Name: TESTDC$
      Primary Domain: TESTDOM
      Primary Logon ID: (0x0,0x3E7)
      Client User Name: ANONYMOUS LOGON
      Client Domain: NT AUTHORITY
      Client Logon ID: (0x0,0x40DE417)
      Accesses  READ_CONTROL 
       
      Privileges  -
     
     Properties: 

    Note  The directory service has been contacted as the anonymous user. This is because the credentials of the Web user cannot be correctly conveyed to the directory service.

ASP .NET Base Account

By default, all ASP.NET applications run under the base process account, MACHINENAME\ASPNET. This is a local account that does not have access to objects in Active Directory. To access Active Directory by using the credentials that are passed to IIS, you must modify your Web.config file to contain the parameters identity impersonate="true" and authentication mode="Windows" . The presence of these two parameters causes ASP.NET to run the code under the credentials that are passed to it by IIS.

Note  This is similar to how classic ASP currently works. A High-Isolated or Out-of-Process (OOP) application is actually running in a separate DllHost process. DllHost's base process is IWAM_machinename. When calls are made to this OOP, it impersonates the user who was authenticated by IIS. With ASP.NET, pages will run in a separate process also, that process being Aspnet_wp.exe. By using the identity impersonate tag, the application designer controls whether that impersonation is performed.

Error That You May Receive If The ASP.NET Base Account Is Not Set Correctly

If the ASP.NET Base account is not set correctly, you may receive one of the following error messages:

Cannot contact the specified domain or domain does not exist
Logon Failure: Unknown Username or bad password

Troubleshoot ASP.NET Base Account

To troubleshoot this problem, check for the following issues.

ADSI Schema Cache

ADSI tries to cache the schema from Active Directory. The schema cache is used to determine how to read the attributes out of the attribute cache. If ADSI cannot cache the schema, it uses a V2 version of the schema. The V2 version of the schema contains a very small set of attributes.

ADSI will try to cache the schema only one time per process. In Windows 2000, ASP.NET runs under a single aspnet_wp.exe process. This means that the schema will not be cached again until the IIS service is shut down and restarted.

Subsequent Schema cache access may depend on the user rights of the first user who runs an ASP.NET page that uses ADSI on that server.

In a typical scenario, an administrator notices that the application works by launching a Web browser locally. The Web site is then made live and works for a period of time until the server is restarted or Web services are restarted.

At this point, the ASP.NET application stops working, because ADSI did not cache the schema correctly. This may happen when the first user to access the Web site cannot establish the credentials to correctly cache the schema. This is a likely scenario when a user suffers from the double-hop problem described earlier in this article. You may not quickly realize that this has happened because you may not see a Permission Denied error message or a Property not Found in Cache error message. This may be due to the method that was used to install the Active Directory. When the first domain controller in the domain is promoted, the DCPromo Wizard asks whether the Domain should be compatible with Windows NT 4 or with only Windows 2000. If you accept the default of Compatible permissions with NT4, the security principal EVERYONE is added to the Pre-Windows 2000 Compatible Access built-in group. This is significant because, by default, the Pre-Windows 2000 Compatible Access group has List Contents and Read All Properties permissions on many objects in the directory. Because the Anonymous user will access the directory service as EVERYONE, that anonymous user will get many attributes returned in a query and loaded into the attribute cache.

The schema that ADSI uses is stored in the cn=Aggregate object in the schema namespace. Neither the Pre-Windows 2000 Compatible Access built-in group nor the Everyone principal have permissions on this aggregate object. Therefore, schema information is not accessible. The result is that there is a property in the attribute cache that was retrieved from the server that ADSI does not understand. Because ADSI cannot determine the data type, you receive the error that is mentioned in the next paragraph.

ADSI Schema Cache Error

You may receive errors. After you restart the server, the Web application does not respond, and you may also receive the following error message:

0x8000500C, "The property in cache cannot be 
converted from native datatype"
This error indicates ADSI is not correctly caching your Active Directory schema.

Correcting an ADSI Schema Cache Problem

Invoking ADSI

ADSI interfaces expose many properties, such as the IADsUser properties LastName and FirstName and methods, such as SetPassword. System.DirectoryServices does not wrap all ADSI interfaces. To access the underlying methods in ADSI, use the Invoke method. For properties, you can use the InvokeMember method. This section provides code examples for using both options.

The following topics are contained in this section:

Invoking ADSI Properties

The following code example shows how to use InvokeMember to retrieve the IADsUser properties FirstName and LastName from a managed code application.

using System.Reflection;
using System.DirectoryServices;
...
DirectoryEntry ent = new DirectoryEntry("LDAP://CN=My User,DC=Fabrikam,DC=com");
Object ads = ent.NativeObject;
Type type = ads.GetType();
String firstName = (string) type.InvokeMember("FirstName", BindingFlags.GetProperty, null, ads, null);
String lastName = (string) type.InvokeMember("LastName", BindingFlags.GetProperty, null, ads, null);

The following code example shows how to set the Description property using InvokeMember.

using System.Reflection;
using System.DirectoryServices;
...
DirectoryEntry ent = new DirectoryEntry("LDAP://CN=My User,DC=Fabrikam,DC=com");
Object ads = ent.NativeObject;
Type type = ads.GetType();
type.InvokeMember("Description", BindingFlags.SetProperty, null, ads, new object[] {"some description"});
de.CommitChanges();

Invoking ADSI Methods

If an ADSI interface supports the IDispatch interface, then you can use the DirectoryEntry method Invoke to access the methods on that interface. This also applies to any ADSI extensions that you may have added in the past. You do not need to include the ADSI library to use the Invoke method.

The following code example shows how to invoke the IADsUser method SetPassword to set a password.

DirectoryEntry usr = new DirectoryEntry("LDAP://CN=John Smith, DC=Fabrikam,DC=COM");
usr.Invoke("SetPassword", new object[] {"secre@t!!!"});

The following code example shows how to invoke the IADsUser method ChangePassword to change a password.

DirectoryEntry usr = new DirectoryEntry("LDAP://CN=John Smith, DC=Fabrikam,DC=COM");
usr.Invoke("ChangePassword", new object[] {"secre@t!!!","mynewsecre@t!#*"});

The following code example shows how to invoke the IADsGroup method Members to retrieve the members of a group.

DirectoryEntry grpEntry = new DirectoryEntry("LDAP://CN=Enterprise Admins,CN=Users,DC=Fabrikam, DC=com");
object members = grpEntry.Invoke("Members",null);
foreach( object member in (IEnumerable) members)
{
    DirectoryEntry x = new DirectoryEntry(member);
    Console.WriteLine(x.Name);
}

Using COM Interop to Access ADSI

System.DirectoryServices redefines selected, commonly used data structures and constants that are provided in ADSI. However, during the transition between COM applications and managed code applications, there will be instances when you will want a managed code application to have access to the features provided by low-level ADSI structures or advanced interfaces. To provide this interoperability between the underlying ADSI and managed code, the Common Language Runtime has provided COM interop. You can use to access any ADSI feature. This topic describes how to use COM interop to access ADSI.

Setting up COM Interop in Your Application

  1. Open Visual Studio .NET and create a new project.
  2. From Project, select Add References... to open the Add Reference dialog.
  3. Select COM tab.
  4. Select Active DS Type Library from the list, as shown in the figure below.

    Graphic showing the Add Reference... window.

  5. Click OK to add the ADSI library.

Instead of a reference you can add a using statement to your application, as shown in the following code example.

using ActiveDs; or
Imports ActiveDs

If you do not add a reference or a using statement, you must add the fully-qualified name for the ADSI library as shown in the following code example.

ActiveDs.IADsSecurityDescriptor

Group Management

This section contains code examples that can be used to add and manage groups on a domain.

For more information, see:

Creating Groups

This topic shows how to create several types of groups.

When you create a new group, you can use flags from the ADS_GROUP_TYPE_ENUM enumeration to assign a group type to the group, such as global (ADS_GROUP_TYPE_GLOBAL_GROUP), domain local (ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP), local (ADS_GROUP_TYPE_LOCAL_GROUP), universal (ADS_GROUP_TYPE_UNIVERSAL_GROUP) or security enabled (ADS_GROUP_TYPE_SECURITY_ENABLED). If you do not specify a group type, the default is to create a global, secured group (ActiveDs.ADS_GROUP_TYPE_ENUM.ADS_GROUP_TYPE_GLOBAL_GROUP | ActiveDs.ADS_GROUP_TYPE_ENUM.ADS_GROUP_TYPE_SECURITY_ENABLED).

The following code example shows how to create a new group, called Practice Managers to the organizational unit, called Consulting.

DirectoryEntry dom = new DirectoryEntry();		// Bind to the domain that this user is currently connected to.
DirectoryEntry ou = dom.Children.Find("OU=Consulting"); // Find the container (in this case, the Consulting organizational unit) that you wish to add the new group to.
DirectoryEntry group = ou.Children.Add("CN=Practice Managers","group");	// Add the new group Practice Managers.
group.CommitChanges();	// Commit the new group to the directory.

The following code example shows how to create a local domain group called Managers to the Consulting organizational unit. Use COM Interop to specify the ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP flag.

DirectoryEntry dom = new DirectoryEntry();		// Bind to the domain that this user is currently connected to.
DirectoryEntry ou = dom.Children.Find("OU=Consulting");
DirectoryEntry mgr = ou.Children.Add("CN=Managers","group");// Add the Managers group.
// Set the group type to a secured domain local group.
mgr.Properties["groupType"].Value = ActiveDs.ADS_GROUP_TYPE_ENUM.ADS_GROUP_TYPE_DOMAIN_LOCAL_GROUP | ActiveDs.ADS_GROUP_TYPE_ENUM.ADS_GROUP_TYPE_SECURITY_ENABLED;
mgr.CommitChanges();

The following code example shows how create a non-security group, which is a distribution list called Full Time Employees to the Consulting organizational unit. Use COM Interop to specify the ADS_GROUP_TYPE_GLOBAL_GROUP flag.

DirectoryEntry dom = new DirectoryEntry();
// Find the container (in this case, the Consulting organizational unit) that you wish to add the Full Time Employees distribution list to.
DirectoryEntry ou = dom.Children.Find("OU=Consulting");
DirectoryEntry dl = ou.Children.Add("CN=Full Time Employees","group");// Add the Full Time Employees distribution list.
dl.Properties["groupType"].Value = ActiveDs.ADS_GROUP_TYPE_ENUM.ADS_GROUP_TYPE_GLOBAL_GROUP;// Set the group type to global.
dl.CommitChanges();

The following code example shows how to add an entire group to another group.

[C#]
// Bind to the domain that this user is currently connected to.
DirectoryEntry dom = new DirectoryEntry();
// Find the container (in this case, the North America group) that you
// wish to add.
DirectoryEntry group = dom.Children.Find("CN=North America");
// Connect to the group that you wish to add "group" to.
DirectoryEntry mgr = new DirectoryEntry("LDAP://CN=Managers,OU=Consulting,DC=Fabrikam,DC=COM");
// Add the distinguishedName of "group" to the members property of "mgr".
mgr.Properties["member"].Add(group.Properties["distinguishedName"].Value);
// Commit the changes to the directory.
mgr.CommitChanges();

Adding Users to a Group

When a group is created, users must be added to the group. This topic describes how to add a user to a group.

The following code example shows how to add a user, new user, to the consulting organization.

[C#]
DirectoryEntry dom = new DirectoryEntry();
DirectoryEntry group = dom.Children.Find("CN=Consulting");
// Add a single user to a group;
DirectoryEntry usr = group.Children.Find("CN=New User");

// To add multiple users to a group use 
//group.Properties["member"].AddRange(new string[] {"userDN1","userDN2"});

//To add the user's distinguished name to the member property
//on the group object, use the Add method.
group.Properties["member"].Add(usr.Properties["distinguishedName"].Value);
//Commit the changes to the directory.
group.CommitChanges();
[Visual Basic .NET]
Dim dom As New DirectoryEntry()
Dim group As DirectoryEntry = dom.Children.Find("CN=Consulting")
' Add a single user to a group;
Dim usr As DirectoryEntry = group.Children.Find("CN=New User")

' To add multiple users to a group use 
'group.Properties("member").AddRange(New String() {"userDN1", "userDN2"})

'To add the user's distinguished name to the member property
'on the group object, use the Add method.
group.Properties("member").Add(usr.Properties("distinguishedName").Value)
'Commit the changes to the directory.
group.CommitChanges()

Removing Users from a Group

The following code example shows how to remove users from a group. For this task, find the user to be removed, which in the example is User Name, then call the Remove method.

[C#]
DirectoryEntry dom = new DirectoryEntry();
DirectoryEntry ou = dom.Children.Find("OU=Consulting");
// Add a user to a group.
DirectoryEntry usr = ou.Children.Find("CN=User Name");String userDN = usr.Properties["distinguishedName"].Value.ToString();
group.Properties["member"].Remove(userDN);
group.CommitChanges();

Enumerating Users in a Group

This topic includes code examples for enumerating the members of a group. If the group has many members, you can get a result set by calling IADsGroup::Members.

The following code example shows how to get members using the SearchResult property Properties.

[C#]
DirectoryEntry group = new DirectoryEntry("LDAP://CN=Sales,DC=Fabrikam,DC=COM");
foreach(object dn in group.Properties["member"] )
{
   Console.WriteLine(dn);
}

The following code example shows how to get members using the Invoke method to call the ADSI IADsGroup::Members method.

[C#]
DirectoryEntry group = new DirectoryEntry("LDAP://CN=Sales,DC=Fabrikam,DC=COM");
object members = group.Invoke("Members",null);
foreach( object member in (IEnumerable) members)
{
DirectoryEntry x = new DirectoryEntry(member);
Console.WriteLine(x.Name);
}

Enumerating Members in a Large Group

This topic explains how range retrieval works and provides several code examples for using range retrieval to obtain the members of a group. The group object contains a property called member which contains multiple values in an array. Because group memberships can sometimes be quite large, this property may contain hundreds of values. Range retrieval is a process of obtaining a portion of the members at a time. For Windows Server 2003 family, the maximum number of values that can be retrieved from the server at one time is 1500. If you set the range retrieval to a value that is higher than the number of values in the set, the search fails. If you set the range to a small number, then you can degrade the performance of the search because it must return to the server for new results more often. For more information about range retrieval, see Enumerating Groups That Contain Many Members.

The following code example shows how to get members of a group using range retrieval. This sample retrieves entries 0-500, inclusively. The maximum entries for this result set are 5001.

[C#]
DirectoryEntry group = new DirectoryEntry("LDAP://CN=Sales,DC=Fabrikam,DC=COM");
DirectorySearcher groupMember = new DirectorySearcher
    (group,"(objectClass=*)",new string[]{"member;Range=0-500"},SearchScope.Base);
SearchResult result = groupMember.FindOne();
// Each entry contains a property name and the path (ADsPath).
// The following code returns the property name from the PropertyCollection. 
String propName=String.Empty;
foreach(string s in result.Properties.PropertyNames)
{
    if ( s.ToLower() != "adspath")
    {
      propName = s;
      break;
    }
}
foreach(string member in result.Properties[propName])
{
     Console.WriteLine(member);
}

You can also use range retrieval to retrieve a portion of the result set by starting and ending at a specified point within the result set. To do this, modify the {"member;Range=0-500"} statement. For example, to retrieve the third and fourth entries in the result set, you would use the statement {"member;Range=2-3"}. To retrieve all entries, starting with 502 to the end of the result set, you would use the statement {"member;Range=501-*"}.

The final code example shows how to use range retrieval to get all the members of the group when you do not know how many members are in the group. Because range retrieval does not work if you try to get more members than are in the result set, this code example tests for a failure and when it receives it, it changes the range statement to ("member;range={0}-*", rangeLow) to enumerate the final members in the set.

[C#]
try
{
    DirectoryEntry entry = new DirectoryEntry("LDAP://CN=My Distribution List,OU=Distribution Lists,DC=Fabrikam,DC=com");
    DirectorySearcher searcher = new DirectorySearcher(entry);
    searcher.Filter = "(objectClass=*)";

    uint rangeStep = 1000;
    uint rangeLow = 0;
    uint rangeHigh = rangeLow + (rangeStep - 1);
    bool lastQuery = false;
    bool quitLoop = false;

    do
    {
        string attributeWithRange;
        if(!lastQuery)
        {
            attributeWithRange = String.Format("member;range={0}-{1}", rangeLow, rangeHigh);
        }
        else
        {
            attributeWithRange = String.Format("member;range={0}-*", rangeLow);
        }           
        searcher.PropertiesToLoad.Clear();
        searcher.PropertiesToLoad.Add(attributeWithRange);
        SearchResult results = searcher.FindOne();
        foreach(string res in results.Properties.PropertyNames)
        {
            System.Diagnostics.Debug.WriteLine(res.ToString());
        }
        if(results.Properties.Contains(attributeWithRange))
        {
            foreach(object obj in results.Properties[attributeWithRange])
            {
                Console.WriteLine(obj.GetType());
                if(obj.GetType().Equals(typeof(System.String)))
                {
                }
                else if (obj.GetType().Equals(typeof(System.Int32)))
                {
                }
                Console.WriteLine(obj.ToString());
            }
            if(lastQuery)
            {
                quitLoop = true;
            }
        }
        else
        {
            lastQuery = true;
        }
        if(!lastQuery)
        {
            rangeLow = rangeHigh + 1;
            rangeHigh = rangeLow + (rangeStep - 1);
        }
    }
    while(!quitLoop);
}
catch(Exception ex)
{
    // Handle exception ex.
}

Searching for Groups

This topic shows how to search for groups using DirectorySearcher.

The following code example shows how to search for all groups on a domain.

[C#]
using System.DirectoryServices;
...
DirectorySearcher src = new DirectorySearcher(ou,"(objectCategory=group)");
foreach(SearchResult res in src.FindAll())
{
    Console.WriteLine(res.Path);
}

The following code example shows how to search for all security enabled groups. For this search, use COM Interop. It uses a bitwise search.

[C#]
using System.DirectoryServices;
...
DirectorySearcher src = new DirectorySearcher(ou,"(objectCategory=group)");
int val = (int) ActiveDs.ADS_GROUP_TYPE_ENUM.ADS_GROUP_TYPE_SECURITY_ENABLED;
string query = "(&(objectCategory=group)(groupType:1.2.840.113556.1.4.804:=" + val.ToString() + "))";
src.Filter = query;
foreach(SearchResult res in src.FindAll())
{
    Console.WriteLine(res.Path);
}

The following code example shows how to search for all global domain groups, regardless of whether they are secure or non-secure. For this search, use COM Interop.

[C#]
using System.DirectoryServices;
...
DirectorySearcher src = new DirectorySearcher(ou,"(objectCategory=group)");
int val = (int) ActiveDs.ADS_GROUP_TYPE_ENUM.ADS_GROUP_TYPE_GLOBAL_GROUP;
string query = "(&(objectCategory=group)(groupType:1.2.840.113556.1.4.804:=" + val.ToString() + "))";
src.Filter = query;
foreach(SearchResult res in src.FindAll())
{
    Console.WriteLine(res.Path);
}

The following code example shows how to search for all global domain, secure groups. For this search, use COM Interop.

[C#]
using System.DirectoryServices;
...
DirectorySearcher src = new DirectorySearcher(ou,"(objectCategory=group)");
int val = (int) (ActiveDs.ADS_GROUP_TYPE_ENUM.ADS_GROUP_TYPE_GLOBAL_GROUP 
    | ActiveDs.ADS_GROUP_TYPE_ENUM.ADS_GROUP_TYPE_SECURITY_ENABLED);
string query = "(&(objectCategory=group)(groupType=" + val.ToString() + "))";
src.Filter = query;
foreach(SearchResult res in src.FindAll())
{
    Console.WriteLine(res.Path);
}

Deleting Groups

The following code example shows how to delete a group using the DirectoryEntries method called Remove. For this task, find the group to delete, which, in the example, is Consulting, then run the Remove method.

[C#]
//Bind to the current domain. 
DirectoryEntry dom = new DirectoryEntry(); 
//Use the Find method to find the Consulting OU
DirectoryEntry ou = dom.Children.Find("OU=Consulting");
//To delete a group, bind to the group within the OU
DirectoryEntry group = new DirectoryEntry(ou + "CN=groupname");
//To delete a manager, bind to the manager object within the OU
DirectoryEntry mgr = new DirectoryEntry(ou + "CN=mgrname");
//To delete a distribution list, bind to the distribution list object
//within the OU
DirectoryEntry dl = new DirectoryEntry(ou + "CN=dlname");
//Use the remove method to remove each of these objects.
ou.Children.Remove(group); 
ou.Children.Remove(mgr); 
ou.Children.Remove(dl);
[Visual Basic .NET]
'Bind to the current domain. 
Dim dom As New DirectoryEntry()
'Use the Find method to find the Consulting OU
Dim ou As DirectoryEntry = dom.Children.Find("OU=Consulting")
'To delete a group, bind to the group within the OU
Dim group As New DirectoryEntry(ou + "CN=groupname")
'To delete a manager, bind to the manager object within the OU
Dim mgr As New DirectoryEntry(ou + "CN=mgrname")
'To delete a distribution list, bind to the distribution list object
'within the OU
Dim dl As New DirectoryEntry(ou + "CN=dlname")
'Use the remove method to remove each of these objects.
ou.Children.Remove(group)
ou.Children.Remove(mgr)
ou.Children.Remove(dl)

User Management

This section contains code examples that can be used to add and manage users on a domain.

When you setup a user object, set several user properties on the object at the same time. For example, there are several flags in the userAccountControl property that are used to control the user password behavior, account lockout, account enable and disable, the home directory, and the type of account that the user has. Several code examples in this section show how to set flags for the userAccountControl. There are also some samples for setting the properties displayed in the System Administrator property pages for each user.

For more information about properties that can be set on the user object during creation, see Creating a User.

The following topics are provided in this section:

Creating Users

The following code example shows how to create a user in an organizational unit. By default, this account will be disabled. To enable the account, set a password for it. For more information, see Managing User Passwords.

[C#]
DirectoryEntry ent = new DirectoryEntry();
DirectoryEntry ou = ent.Children.Find("OU=Consulting");

// Use the Add method to add a user in an organizational unit.
DirectoryEntry usr = ou.Children.Add("CN=New User","user");
// Set the samAccountName, then commit changes to the directory.
usr.Properties["samAccountName"].Value = "newuser"; 
usr.CommitChanges();

The samAccountName property is set in this code example. The samAccountName creates a unique samAccountName, such as $CP2000-O16B1V0UKHK7. This property is required on the user account when the domain controller is running on a Windows NT 4.0 server. In Windows Server 2003, the samAccountName property is optional.

Enabling and Disabling the User Account

This topic provides code examples for enabling and disabling a user account. It uses the Properties method to access the userAccountControl property to set the ADS_UF_ACCOUNTDISABLE flag which is defined in the ADS_USER_FLAG_ENUM.

The following code example shows how to enable a user account.

[C#]
DirectoryEntry usr = new DirectoryEntry("LDAP://CN=New User,CN=users,DC=fabrikam,DC=com");
int val = (int) usr.Properties["userAccountControl"].Value;
usr.Properties["userAccountControl"].Value = val & ~ADS_UF_ACCOUNTDISABLE;
usr.CommitChanges();

The following code example shows how to disable a user account.

[C#]
DirectoryEntry usr = new DirectoryEntry("LDAP://CN=Old User,CN=users,DC=fabrikam,DC=com");
int val = (int) usr.Properties["userAccountControl"].Value;
usr.Properties["userAccountControl"].Value = val | ADS_UF_ACCOUNTDISABLE;
usr.CommitChanges();

Setting a User Account Expiration

This code example shows how to set the expiration on a user account.

This operation uses the InvokeMember method to access the IADsUser property AccountExpirationDate.

[C#]
using System.Reflection;
... 
Type type = usr.NativeObject.GetType();
Object adsNative = usr.NativeObject;
type.InvokeMember("AccountExpirationDate", BindingFlags.SetProperty, null, 
adsNative, new object[]{"12/29/2004"});
usr.CommitChanges();

Managing User Passwords

This topic includes information and code examples for managing user passwords.

The following code example shows how to set the user password by invoking the IADsUser::SetPassword method.

usr.Invoke("SetPassword", new object[]{"secret"});

The following code example shows how to change the user password by invoking the IADsUser::ChangePassword method.

usr.Invoke("ChangePassword",new object[]{"oldpass","newpass"});

The following code example shows how to set the user password so that it must be changed at the next logon. It sets the pwdLastSet property to on (0).

usr.Properties["pwdLastSet"].Value = 0; // To turn off, set this value to -1.
usr.CommitChanges();
        or
Dim objUser As ActiveDs.IADsUser = CType(UserEntry.NativeObject, ActiveDs.IADsUser)
objUser.Put("pwdLastSet", 0)
objUser.SetInfo()

Pwd-Last-Set: The date and time that the password for this account was last changed. This value is stored as a large integer that represents the number of 100 nanosecond intervals since January 1, 1601 (UTC). If this value is set to 0 and the User-Account-Control attribute does not contain the UF_DONT_EXPIRE_PASSWD flag, then the user must set the password at the next logon.

User-Account-Control: Flags that control the behavior of the user account. 

This attribute value can be zero or a combination of one or more of the following values.
Hexadecimal value Identifier (defined in iads.h) Description
0x00000001 ADS_UF_SCRIPT The logon script is executed.
0x00000002 ADS_UF_ACCOUNTDISABLE The user account is disabled.
0x00000008 ADS_UF_HOMEDIR_REQUIRED The home directory is required.
0x00000010 ADS_UF_LOCKOUT The account is currently locked out.
0x00000020 ADS_UF_PASSWD_NOTREQD No password is required.
0x00000040 ADS_UF_PASSWD_CANT_CHANGE The user cannot change the password.
0x00000080 ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED The user can send an encrypted password. Store password using reversible encryption
0x00000100 ADS_UF_TEMP_DUPLICATE_ACCOUNT This is an account for users whose primary account is in another domain. This account provides user access to this domain, but not to any domain that trusts this domain. Also known as a local user account.
0x00000200 ADS_UF_NORMAL_ACCOUNT This is a default account type that represents a typical user.
0x00000800 ADS_UF_INTERDOMAIN_TRUST_ACCOUNT This is a permit to trust account for a system domain that trusts other domains.
0x00001000 ADS_UF_WORKSTATION_TRUST_ACCOUNT This is a computer account for a computer that is a member of this domain.
0x00002000 ADS_UF_SERVER_TRUST_ACCOUNT This is a computer account for a system backup domain controller that is a member of this domain.
0x00004000 N/A Not used.
0x00008000 N/A Not used.
0x00010000 ADS_UF_DONT_EXPIRE_PASSWD The password for this account will never expire.
0x00020000 ADS_UF_MNS_LOGON_ACCOUNT This is an MNS logon account.
0x00040000 ADS_UF_SMARTCARD_REQUIRED The user must log on using a smart card.
0x00080000 ADS_UF_TRUSTED_FOR_DELEGATION The service account (user or computer account), under which a service runs, is trusted for Kerberos delegation. Any such service can impersonate a client requesting the service.
0x00100000 ADS_UF_NOT_DELEGATED The security context of the user will not be delegated to a service even if the service account is set as trusted for Kerberos delegation.
0x00200000 ADS_UF_USE_DES_KEY_ONLY Restrict this principal to use only Data Encryption Standard (DES) encryption types for keys.
0x00400000 ADS_UF_DONT_REQUIRE_PREAUTH This account does not require Kerberos pre-authentication for logon.
0x00800000 ADS_UF_PASSWORD_EXPIRED The user password has expired. This flag is created by the system using data from the Pwd-Last-Set attribute and the domain policy.
0x01000000 ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION The account is enabled for delegation. This is a security-sensitive setting; accounts with this option enabled should be strictly controlled. This setting enables a service running under the account to assume a client identity and authenticate as that user to other remote servers on the network.

The following code example shows a function that sets an ACE to deny a password change. It uses COM Interop to access the IADsSecurityDescriptor to get the ntSecurityDescriptor property. It then uses the IADsAccessControlList to get the DACL from the security descriptor and IADsAccessControlEntry to get the AceType, AceFlags, Trustee, Flags, ObjectType and AccessMask properties. The AceType flags are defined in ADS_ACETYPE_ENUM. The AceFlags are defined in the ADS_FLAGTYPE_ENUM. AccessMask flags are defined in the ADS_RIGHTS_ENUM.

[Visual Basic .NET]
Imports System
Imports System.DirectoryServices
Imports ActiveDs
...
Shared Sub DenyChangePassword(User As DirectoryEntry)
      Const PASSWORD_GUID As String = "{ab721a53-1e2f-11d0-9819-00aa0040529b}"
      Const ADS_UF_ACCOUNTDISABLE As Integer = 2
      Const ADS_UF_PASSWORD_EXPIRED As Integer = &H800000
      Const ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION As Integer = &H1000000
      
      
      Dim trustees() As String = {"NT AUTHORITY\SELF", "EVERYONE"}
      
      Dim sd As ActiveDs.IADsSecurityDescriptor = CType(User.Properties("ntSecurityDescriptor").Value, 
          ActiveDs.IADsSecurityDescriptor)
      Dim acl As ActiveDs.IADsAccessControlList = CType(sd.DiscretionaryAcl, 
          ActiveDs.IADsAccessControlList)
      Dim ace As New ActiveDs.AccessControlEntry()
      
      Dim trustee As String
      For Each trustee In  trustees
         ace.Trustee = trustee
         ace.AceFlags = 0
         ace.AceType = Fix(ActiveDs.ADS_ACETYPE_ENUM.ADS_ACETYPE_ACCESS_DENIED_OBJECT)
         ace.Flags = Fix(ActiveDs.ADS_FLAGTYPE_ENUM.ADS_FLAG_OBJECT_TYPE_PRESENT)
         ace.ObjectType = PASSWORD_GUID
         ace.AccessMask = Fix(ActiveDs.ADS_RIGHTS_ENUM.ADS_RIGHT_DS_CONTROL_ACCESS)
         acl.AddAce(ace)
      Next trustee
      sd.DiscretionaryAcl = acl
      User.Properties("ntSecurityDescriptor").Value = sd
      User.CommitChanges()
   End Sub 'DenyChangePassword
[C#]
using System;
using System.DirectoryServices;
using ActiveDs;
...
static void DenyChangePassword(DirectoryEntry User)
{
     const string PASSWORD_GUID = "{ab721a53-1e2f-11d0-9819-00aa0040529b}";
     const int ADS_UF_ACCOUNTDISABLE=2;
     const int ADS_UF_PASSWORD_EXPIRED=0x800000;
     const int ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION=0x1000000;
								
     string[] trustees = new string[]{@"NT AUTHORITY\SELF","EVERYONE"};
				
     ActiveDs.IADsSecurityDescriptor sd = (ActiveDs.IADsSecurityDescriptor)
        User.Properties["ntSecurityDescriptor"].Value;
     ActiveDs.IADsAccessControlList acl = (ActiveDs.IADsAccessControlList) sd.DiscretionaryAcl;
     ActiveDs.IADsAccessControlEntry ace = new ActiveDs.AccessControlEntry();	

     foreach(string trustee in trustees)
     {
          ace.Trustee = trustee;
          ace.AceFlags = 0;
          ace.AceType = (int)ActiveDs.ADS_ACETYPE_ENUM.ADS_ACETYPE_ACCESS_DENIED_OBJECT;
	          ace.Flags = (int)ActiveDs.ADS_FLAGTYPE_ENUM.ADS_FLAG_OBJECT_TYPE_PRESENT;
          ace.ObjectType = PASSWORD_GUID;
          ace.AccessMask = (int)ActiveDs.ADS_RIGHTS_ENUM.ADS_RIGHT_DS_CONTROL_ACCESS;
          acl.AddAce(ace);
     }
     sd.DiscretionaryAcl = acl;
     User.Properties["ntSecurityDescriptor"].Value = sd;
     User.CommitChanges();
}

The following code example shows how to set the password to never expire. It uses the Properties method to access the userAccountControl property to set the ADS_UF_DONT_EXPIRE_PASSWD flag defined in the ADS_USER_FLAG_ENUM.

[Visual Basic .NET]
 Shared Sub DontExpirePassword(User As DirectoryEntry)
 Dim val As Integer
 Const ADS_UF_DONT_EXPIRE_PASSWD As Integer = &H10000
 val = Fix(User.Properties("userAccountControl").Value)
 User.Properties("userAccountControl").Value = val Or ADS_UF_DONT_EXPIRE_PASSWD
 User.CommitChanges()
 End Sub 'DontExpirePassword
[C#]
using System;
using System.DirectoryServices;
using ActiveDs;
...
static void DontExpirePassword(DirectoryEntry User)
{
     int val;
     const int ADS_UF_DONT_EXPIRE_PASSWD =0x10000;
     val = (int) User.Properties["userAccountControl"].Value;
     User.Properties["userAccountControl"].Value = val | 
     ADS_UF_DONT_EXPIRE_PASSWD;
     User.CommitChanges();
}

Setting User Account Flags

This topic contains code examples that set various user flags. It uses the Properties method to access the userAccountControl property to set flags defined in the ADS_USER_FLAG_ENUM.

The following code example shows how to require that a SmartCard be used for an interactive logon.

[C#]
val= (int) usr.Properties["userAccountControl"].Value;
usr.Properties["userAccountControl"].Value = val | 
ADS_UF_SMARTCARD_REQUIRED;
usr.CommitChanges();

The following code example shows how to set the account to use a DES encryption type.

[C#]
const int ADS_UF_USE_DES_KEY_ONLY=0x200000;
val= (int) usr.Properties["userAccountControl"].Value;
usr.Properties["userAccountControl"].Value = val | ADS_UF_USE_DES_KEY_ONLY;
usr.CommitChanges();

The following code example shows how to set the account so that it is trusted for delegation.

[C#]
const int ADS_UF_TRUSTED_FOR_DELEGATION =0x80000;
val= (int) usr.Properties["userAccountControl"].Value;
usr.Properties["userAccountControl"].Value = val | 
ADS_UF_TRUSTED_FOR_DELEGATION;
usr.CommitChanges();

The following code example shows how to show that the account is sensitive and cannot be used for delegation.

[C#]
const int ADS_UF_NOT_DELEGATED=0x100000;
val= (int) usr.Properties["userAccountControl"].Value;
usr.Properties["userAccountControl"].Value = val | ADS_UF_NOT_DELEGATED;
usr.CommitChanges();

The following code example shows how to set the account so that it does not require Kerberos pre-authentication.

[C#]
const int ADS_UF_DONT_REQUIRE_PREAUTH=0x400000;
val= (int) usr.Properties["userAccountControl"].Value;
usr.Properties["userAccountControl"].Value = val | 
ADS_UF_DONT_REQUIRE_PREAUTH;
usr.CommitChanges();

Setting Properties Displayed on Property Pages

This topic provides code examples for setting the properties that appear in the property pages for users, which is accessed through the Active Directory Users and Computers MMC snap-in. If you have not used this snap-in before, you can open it on the server by selecting Start>All Programs>Administrative Tools>Active Directory Users and Computers. In the Users folder, right-click on a user name and select Properties. If you are setting up an application that allows a user to change information such as a home address, this is a useful piece of code to add to your application.

The following code example shows how to set common properties displayed in the General page.

[C#]
usr.Properties["givenName"].Value = "New User";
usr.Properties["initials"].Value = "Ms";
usr.Properties["sn"].Value = "Name";
usr.Properties["displayName"].Value = "New User Name";
usr.Properties["description"].Value = "Vice President-Operation";
usr.Properties["physicalDeliveryOfficeName"].Value = "40/5802";
usr.Properties["telephoneNumber"].Value = "(425)222-9999";
usr.Properties["mail"].Value = "newuser@fabrikam.com";
usr.Properties["wWWHomePage"].Value = "http://www.fabrikam.com/newuser";
usr.Properties["otherTelephone"].AddRange(new 
string[]{"(425)111-2222","(206)222-5263"});
usr.Properties["url"].AddRange(new 
string[]{"http://newuser.fabrikam.com","http://www.fabrikam.com/officers"});
usr.CommitChanges();

The following code example shows how to set common properties displayed in the Address page.

[C#]
usr.Properties["streetAddress"].Value = "2050 Fabrikam Way NE";
usr.Properties["postOfficeBox"].Value = "S/2523";
usr.Properties["l"].Value = "Sammamish";
usr.Properties["st"].Value = "Washington";
usr.Properties["postalCode"].Value = "98074";
usr.Properties["c"].Value = "US";
usr.CommitChanges();

The following code example shows how to set common properties displayed in the Account page.

[C#]
usr.Properties["userPrincipalName"].Value = "newuser@fabrikam.com";
usr.Properties["sAMAccountName"].Value = "newuser";
usr.Properties["userWorkstations"].Value = "wrkst01,wrkst02,wrkst03";
usr.CommitChanges();

Enumerating User Memberships

This topic includes information and a code example that shows how to use a Windows form to enumerate user memberships.

To create a Windows form to display user memberships

  1. Open Visual Studio .NET and select New Project.
  2. From Project Types, select Visual C# or Visual Basic and from Templates, select Windows Application.
  3. Name the new project and select OK.
  4. Select Project>Add Reference and select System.DirectoryServices from the list displayed on the .NET tab.
  5. On the form design page, drag a text box from the Toolbox to the form and format it. This is where the user will add a user name to bind to.
  6. Drag a label from the Toolbox to the form and modify the Text property to read "Enter Name:"
  7. Drag a button from the Toolbox to the form and modify the Text property to read "Find Groups".
  8. Drag a ListBox from the Toolbox to the form. This is where the results will be displayed.
  9. Double-click the form to go to the code page.
  10. Add the "using System.DirectoryServices;" statement to the end of the using statement list.
  11. Add the code example, that follows this procedure, to main.
  12. Compile and run the application.

The following code example shows how to use a Windows form to enumerate user memberships.

[C#]
static void Main() 
{
    Application.Run(new Form1());
}
private void label1_Click(object sender, System.EventArgs e)
{
}
private void textBox1_TextChanged(object sender, System.EventArgs e)
{
}
private void button1_Click(object sender, System.EventArgs e)
{
    string strUserADsPath = "LDAP://fabrikam/cn=" +textBox1.Text +",cn=users,dc=fabrikam,dc=com";
    DirectoryEntry oUser;
    oUser = new DirectoryEntry(strUserADsPath);
    listBox1.Items.Add("Groups to which {0} belongs:"+ oUser.Name);
    // Invoke IADsUser::Groups method.
    object groups = oUser.Invoke("Groups");
    foreach ( object group in (IEnumerable)groups)   
    {
        // Get the Directory Entry.
        DirectoryEntry groupEntry  = new DirectoryEntry(group);
        listBox1.Items.Add(groupEntry.Name); 
    }
}
    private void Form1_Load(object sender, System.EventArgs e)
    {		
    }	
}