Forms Authentication and the Active Directory membership provider

We have reports published to SQL Server Reporting Services, and we want users to access those reports from a web application. We could give them direct access to Report Manager, but we choose not to. The Report Manager UI displays concepts in the reporting domain, not concepts in our problem domain (healthcare). We want to give the users a simpler, branded experience, while still giving them access to reports created by a business administrator, not a developer.

SSRS uses Active Directory for authentication. While it is theoretically possible to change the authentication provider, this is exceedingly difficult. In prior releases, SSRS ran both the Report Manager and the Web Service in IIS, which lets you to choose to allow anonymous access. As of SQL Server 2008, SSRS hosts these services itself. It does not expose the anonymous access option. So even if you change the authentication provider for the reports, you must first get past the web server security. The net effect is that your users need to be in Active Directory.

Fortunately for us, we use Active Directory for authentication. We just don’t use Windows Authentication for the web application. That makes this design feasible.

Use Forms Authentication
When you create a new ASP .NET web application, it is initially configured to use Windows Authentication. This allows a user within the domain to use their credentials to access the application without re-authenticating. Since they are logged in to Windows, those credentials get passed through to the app.

In our case, however, our users are not logged in to our domain. They access the application over the Internet. They may not even be running Windows. So we have to use Forms Authentication.

Even though our users aren’t on our domain, we still use Active Directory as an identity store. Fortunately, it’s possible to use Forms Authentication with Active Directory.

Create a new ASP.NET Web Application project. This can also be done with MVC, but our existing application was written prior to its release. Add a new Web Form to the project called “Login.aspx”. Add an asp:Login control to the page. You can just drag one from the toolbox.

Double-click the login control to handle the Authenticate event. This event will be called when the user presses the Login button. For now, we’ll use FormsAuthentication.Authenticate. We’ll change that in a little bit.

<body>
    <form id="form1" runat="server">
    <div>
    <asp:Login ID="Login1" runat="server" onauthenticate="Login1_Authenticate">
    </asp:Login>
    </div>
    </form>
</body>
protected void Login1_Authenticate(object sender, AuthenticateEventArgs e)
{
    e.Authenticated = FormsAuthentication.Authenticate(Login1.UserName, Login1.Password);
}

Edit the web.config file to use forms authentication. Create a user credential to make sure everything is working so far.

    <!-- MLP: Changed this from "Windows" to "Forms". -->
    <authentication mode="Forms">
      <!-- MLP: Added Login.aspx and .ASPXFORMSAUTH settings. -->
      <forms loginUrl="Login.aspx" name=".ASPXFORMSAUTH">
        <credentials passwordFormat="Clear">
          <user name="test" password="pass"/>
        </credentials>
      </forms>
    </authentication>
    <!-- MLP: Deny unauthenticated users access to other pages. -->
    <authorization>
      <deny users="?" />
    </authorization>

Hit F5 and test your site. You should be redirected from your default page to the login page. If you enter the wrong credentials, you’ll get an error. If you enter user “test” and password “pass”, you’ll get to your default page.

Set up the Active Directory membership provider

Now that we have forms authentication working, let’s switch to using membership. “Membership” is a provider-based system for managing identity and role-based security. By default, membership uses a SQL database to store credentials. You can switch to the Active Directory provider instead.

Active Directory is very similar to a relational database. You access it via a connection string. Access is restricted to specific users. The main difference is that AD is a hierarchical store, while a database is a relational store. AD is typically used to store information about users, groups, and machines within a domain. That’s what the Active Directory membership provider expects.

If you didn’t set up AD yourself, you will need to talk to the person who did. Get a connection string, username, and password that gives you read-only access to the server. If you would like to try this yourself before involving your network operator, you can set up a virtual network in Microsoft Virtual PC.

To configure the Active Directory membership provider, add this to web.config after the <authorization> tag you added earlier. Pay close attention to the enablePasswordReset and attributeMapUsername settings. These are not mentioned in the Patterns and Practices guidance, but I found them to be necessary while working in my environment. The AD account that I have does not have permission to reset passwords. And my company’s directory does not set the userPrincipleName, which is the default.

    <!-- MLP: Use the Active Directory membership provider. -->
    <membership defaultProvider="ADMembershipProvider">
      <providers>
        <add
           name="ADMembershipProvider"
           type="System.Web.Security.ActiveDirectoryMembershipProvider, System.Web, Version=2.0.0.0,
             Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
           connectionStringName="ADConnectionString"
           connectionUsername="MyDomain\MyADUserName"
           connectionPassword="MyADPassword"
           enablePasswordReset="false"
           attributeMapUsername="sAMAccountName"/>
      </providers>
    </membership>

Replace MyDomain and MyADUserName with the correct values. Remove the <credentials> element from <forms>. You won’t need the hard-coded credentials any more. Add the connection string to the top of the file:

  <connectionStrings>
    <!-- MLP: Added connection string for Active Directory authentication. –>
    <add name="ADConnectionString" connectionString="LDAP://MyADMachine/DC=MyDomain,DC=MyTLD" />
  </connectionStrings>

Again, replace MyADMachine, MyDomain, and MyTLD with the values you get from your network admin. You might already have a <connectionStrings> section at the top. Just make sure you have a closing tag and insert the <add …> line.

Finally, we need to change the code to use Membership instead of FormsAuthentication. Change the code that you added before to Login1_Authenticate.

protected void Login1_Authenticate(object sender, AuthenticateEventArgs e)
{
    e.Authenticated = Membership.ValidateUser(Login1.UserName, Login1.Password);
}

Now run the program again and try to log in with your network credentials. In fact, you can delete your onauthenticate handler altogether. The line of code above is exactly what the login control does by default.

Next steps

Now that we are logged in using an Active Directory account, we’ll access an SSMS report using those credentials.

5 Responses to “Forms Authentication and the Active Directory membership provider”

  1. shrikant Says:

    Hi,
    I have a web page through this page when I try to add a new user then users created successfully but when try resetting their password then I am getting errors’

    add New user successfully

    public static void AddUser(ADUser adUser)
    {
    // Local variables
    DirectoryEntry oDE = null;
    DirectoryEntry oDENewUser = null;
    DirectoryEntries oDEs = null;

    try
    {
    oDE = GetDirectoryEntry(GetADPath(PROD, adUser.UserType));

    // 1. Create user account
    oDEs = oDE.Children;
    oDENewUser = oDEs.Add("CN=" + adUser.UserName, "user");

    // 2. Set properties
    SetProperty(oDENewUser, "givenName", adUser.FirstName);
    SetProperty(oDENewUser, "sn", adUser.LastName);
    SetProperty(oDENewUser, "mail", adUser.Email);
    SetProperty(oDENewUser, "sAMAccountName", adUser.UserName);
    oDENewUser.CommitChanges();

    /// 4. Enable account
    EnableAccount(oDENewUser);

    // 3. Set password
    //SetPassword(oDENewUser, adUser.Password);
    SetPassword1(oDENewUser.Path, adUser.Password);
    oDENewUser.CommitChanges();

    oDENewUser.Close();
    oDE.Close();
    }
    catch (Exception ex)
    {
    throw ex;
    }
    }
    I have try the following 2 SetPassword methods but getting error.
    Method 1.
    internal static void SetPassword1(string path, string userPassword)
    {
    //Local variables
    DirectoryEntry usr = null;

    try
    {
    usr = new DirectoryEntry();
    usr.Path = path;
    usr.AuthenticationType = AuthenticationTypes.Secure;
    object ret = usr.Invoke("SetPassword", userPassword);
    usr.CommitChanges();
    usr.Close();
    }
    catch (Exception ex)
    {
    throw ex;
    }
    }
    The exception raised (Error Code 80072035: The server is unwilling to process the request)
    Method 2.
    internal static void SetPassword(DirectoryEntry de, string userPassword)
    {
    //Local variables
    //DirectoryEntry usr = null;
    string quotePwd;
    byte[] pwdBin;

    try
    {
    quotePwd = String.Format(@"""{0}""", userPassword);
    pwdBin = System.Text.Encoding.Unicode.GetBytes(quotePwd);
    de.Properties["unicodePwd"].Value = pwdBin;
    de.CommitChanges();
    //usr.Close();
    }
    catch (Exception ex)
    {
    throw ex;
    }
    }
    The exception raised ("Exception has been thrown by the target of an invocation.")
    Is there an easy way to tell if there is a problem with changing a password?
    Please reply me as soon as possible.
    Thanks.

  2. Michael L Perry Says:

    Whenever you see "Exception has been thrown by the target of an invocation", you need to look at the inner exception. That should give you more information.

    My guess is that the account that your application is running as does not have access to change passwords in Active Directory. I haven't tried this scenario myself, so I'm not sure which account you should use instead. Perhaps you could try impersonating an admin account. Or maybe you can even impersonate the user themselves given their current password as credentials.

  3. shrikant Says:

    Hi All,
    We are able to create new user successfully on the active directory but the SetPassword method is taking around 1.5 minutes to complete the process. Below is the code of the snippet of SetPassword. Is there any better approach to set the password of new user?
    #region SetPassword
    ///
    /// This function is used to set user password
    ///
    ///
    ///
    ///

    ///
    internal static void SetPassword(string path, string userPassword)
    {
    if (_logger.IsDebugEnabled)
    _logger.Debug("ADHelper.cs : Enter SetPassword");

    try
    {
    using (DirectoryEntry usr = GetDirectoryEntry(path))
    {
    object ret = usr.Invoke("SetPassword", userPassword);
    usr.CommitChanges();
    usr.Close();
    }

    if (_logger.IsDebugEnabled)
    _logger.Debug("ADHelper.cs : Exit SetPassword");
    }
    catch (Exception ex)
    {
    if (_logger.IsErrorEnabled)
    _logger.Error("ADHelper.cs : Exception occurred in SetPassword. Message: ", ex);

    throw ex;
    }
    }

    #endregion
    Here is our production environment type.
    • IIS 7
    • ASP.NET 3.5 (C#)
    • Active Directory
    • Windows Server 2008 R2

    Add user snippet
    #region AddUser

    ///
    /// This function is used to add user to active directory
    ///
    /// Active Directory
    /// directory entry object
    ///
    ///
    public static void AddUser(ADUser adUser)
    {
    if (_logger.IsDebugEnabled)
    _logger.Debug("ADHelper.cs : Enter AddUser");

    // Local variables
    DirectoryEntry oDE = null;
    DirectoryEntry oDENewUser = null;
    DirectoryEntries oDEs = null;

    try
    {
    oDE = GetDirectoryEntry(GetADPath(Constants.EnvironmentType.PROD, adUser.UserType));

    // 1. Create user account
    oDEs = oDE.Children;
    oDENewUser = oDEs.Add(string.Format("{0}=", Constants.ADAttributes.CN) + adUser.UserName, "user");

    // 2. Set properties
    SetProperty(oDENewUser, Constants.ADAttributes.givenName, adUser.FirstName);
    SetProperty(oDENewUser, Constants.ADAttributes.sn, adUser.LastName);
    SetProperty(oDENewUser, Constants.ADAttributes.mail, adUser.Email);
    SetProperty(oDENewUser, Constants.ADAttributes.sAMAccountName, adUser.UserName);
    oDENewUser.CommitChanges();

    // 3. Set password
    SetPassword(oDENewUser.Path, adUser.Password);

    // 4. Enable account
    EnableAccount(oDENewUser);

    oDENewUser.Close();
    oDE.Close();

    if (_logger.IsDebugEnabled)
    _logger.Debug("ADHelper.cs : Exit AddUser");
    }
    catch (Exception ex)
    {
    if (_logger.IsErrorEnabled)
    _logger.Error("ADHelper.cs : Exception occurred in AddUser. Message: ", ex);

    throw ex;
    }
    finally
    {
    if (oDENewUser != null)
    {
    oDENewUser.Dispose();
    oDENewUser = null;
    }

    if (oDEs != null)
    {
    oDEs = null;
    }

    if (oDE != null)
    {
    oDE.Dispose();
    oDE = null;
    }
    }
    }

    #endregion

    THANKS IN ADVANCE!!!

  4. Michael L Perry Says:

    A minute and a half! Holy cow! I don't see any reason for that performance problem in your code. You should talk to your network admin. Also, install AD in a VM and test it yourself.

  5. Leonel dos Anjos Says:

    Hi,
    i am developing a system that conects to AD, and i have done it fine by code,
    i have textbox txtUserName the get Authenticated User ond page Load,

    ond visual studio no problem, it takes the current user, but when a deploy i receive Network Service;

    i have in Web.config,
    i Have configured ? user but , i still get this error,

    the code i used to get the user is
    IIdentity identity = System.Security.Principal.WindowsIdentity.GetCurrent(true);

    i can identity.name = "domain\\user" on debug machine.

    But after deployment

    txtUserName.Text = "Network Service";

    Best regards.

Leave a Reply

You must be logged in to post a comment.