Active Directory Domain Services Authentication from ASP .NET

This topic demonstrates how an ASP.NET application can use Forms authentication to permit users to authenticate against Active Directory Domain Services 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.

  5. Leave the default https://localhost in the Server box if you are using the local server. Otherwise, add the path to your server. 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. Under Templates, click Class.

  3. Type LdapAuthentication.cs 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.Web.Security;
    
    using System.Security.Principal;    
    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;
            }
    
            //Update the new path to the user in the directory.
            _path = result.Path;
            _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 Domain Services. 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 to bind 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. For more information about sAMAccountName in the Active Directory Domain Services Schema, see "sAMAccountName" or "SAM-Account-Name attribute" in the MSDN Library. 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. For more information about memberOf in the Active Directory Domain Services Schema, see "memberOf" or "Is-Member-Of-DL attribute" in the MSDN Library. This method returns a list of groups that are 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

The LdapAuthentication.GetGroups method can return a very long string. If the length of this string is greater than the length of the cookie, the authentication cookie might not be created. If this string can potentially exceed the length of the cookie, you might want to store the group information in the ASP.NET Cache object or in a database. Alternatively, you might 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 for theGlobal.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)
      {
        //There is no authentication cookie.
        return;
      }
      FormsAuthenticationTicket authTicket = null;
      try
      {
        authTicket = FormsAuthentication.Decrypt(authCookie.Value);
      }
      catch(Exception ex)
      {
        //Write the exception to the Event Log.
        return;
      }
    if(null == authTicket)
      {
        //Cookie failed to decrypt.
        return;
      }
      //When the ticket was created, the UserData property was assigned a
      //pipe-delimited string of group names.
      string[] groups = authTicket.UserData.Split(new char[]{'|'});
      //Create an Identity.
      GenericIdentity id = new GenericIdentity(authTicket.Name, "LdapAuthentication");
      //This principal flows throughout the request.
      GenericPrincipal principal = new GenericPrincipal(id, groups);
      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 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 impersonate="true"/>
      </system.web>
     </configuration>
    

Notice the following configuration element:

<identity impersonate="true"/>

This element 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 Domain Services, but the account that accesses the Active Directory Domain Services is the configured account.

To configure IIS for anonymous authentication

  1. In the IIS Manager (in Administrative Tools) or the MMC snap-in for IIS, right-click the Web site for which you want to configure authentication, and then click Properties.

  2. Click the Directory Security tab, and then under Authentication and access control, click Edit.

  3. Select the Anonymous Authentication check box (labeled Enable anonymous access in Windows Server 2003).

  4. Make the anonymous account for the application an account that has permission to Active Directory Domain Services.

  5. Clear the Allow IIS To Control Password check box, if it is present. The default IUSR*_<computername>* account does not have permission to Active Directory Domain Services.

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. In the Name box, type Logon.aspx, 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:

    <%@ 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://" + txtDomain.Text;
    
      LdapAuthentication adAuth = new LdapAuthentication(adPath);
      try
      {
        if(true == adAuth.IsAuthenticated(txtDomain.Text, txtUsername.Text, txtPassword.Text))
        {
          string groups = adAuth.GetGroups(txtDomain.Text, txtUsername.Text, txtPassword.Text);
    
    
          //Create the ticket, and add the groups.
          bool isCookiePersistent = chkPersist.Checked;
          FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(1, 
                    txtUsername.Text,DateTime.Now, DateTime.Now.AddMinutes(60), isCookiePersistent, groups);
    
          //Encrypt the ticket.
          string encryptedTicket = FormsAuthentication.Encrypt(authTicket);
    
          //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;
    
          //Add the cookie to the outgoing cookies collection.
          Response.Cookies.Add(authCookie);
    
          //You can redirect now.
          Response.Redirect(FormsAuthentication.GetRedirectUrl(txtUsername.Text, false));
        }
        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:

    <%@ 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.

See Also

Reference

System.DirectoryServices
DirectoryEntry
DirectorySearcher

Concepts

Advanced Programming Topics

Send comments about this topic to Microsoft.

Copyright © 2007 by Microsoft Corporation. All rights reserved.