ONDotNet.com    
 Published on ONDotNet.com (http://www.ondotnet.com/)
 See this if you're having trouble printing code examples


Effective Forms Authentication, Part 2

by Mike Gunderloy
02/17/2004

In the first article in this series, I introduced the basics of forms authentication in ASP.NET. By the end of the article, you saw how to use code in a login page to authenticate users according to whatever custom scheme you like, and how to use additional code in the global.asax file to build custom principal and identity objects to fully identify the application's users and their roles. The earlier solution, while complete, is a bit unsatisfying.

To reuse the authentication code in more than one application requires cutting and pasting both the login code and the global authentication code. As you know, reuse by cut-and-paste is a dangerous practice; if you discover a bug in your code, you have to run around and fix it everywhere. Surely a modern development environment such as ASP.NET can support a more structured type of reuse. In fact, it can.

In this article I'll tidy up the authentication code by making it possible to reuse both chunks of code, using a web custom-control for the initial login, and a HttpModule to build the identity and principal objects. If you haven't run into these parts of ASP.NET yet, you'll end up with two additional techniques to add to your repertoire, which is always a good thing.

Reuse in the ASP.NET User Interface

ASP.NET actually offers you four different alternatives for encapsulating chunks of user interface and code together in controls:

Each of these control types has its pros and cons. Web user controls are extremely easy to create, but they can't be added to the Visual Studio .NET toolbox. And they require their .ascx file to be copied to the project where the controls are used, which puts you right back to cut-and-paste reuse. Composite custom controls are a good choice when you've got a group of controls that you want to cart around together. Derived custom controls are best when you want a new control whose behavior is close to that of a single existing control. And finally, writing from scratch provides you the most flexibility, at the cost of doing the most work.

In the case at hand, I'd like to wrap up the labels and textboxes in the login interface (shown in Figure 1) into a single control. A composite custom control is just the ticket for this.

The login user interface
Figure 1. The login user interface.

Creating the Custom Control

Creating a composite control in C# starts with create a new Web Control Library project; I named the project LoginControls, though right at the moment I don't plan to add more than one control to it. Visual Studio .NET will automatically create a new WebControl1.cs file; I renamed this to LoginControl.cs. Then I went in and gutted it, removing almost all of the automatically generated code. That's because the C# version of the Web Control Library project assumes that you're the type of real programmer who always builds from scratch, and so it includes the scaffolding to create a brand new control rather than deriving from existing controls. That's nice, but it's also not what I wanted. Instead, I added this code to wrap the login bits up as a control:


using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
using System.Security.Principal;
using System.Web.Security;

namespace LoginControls
{
  public class LoginControl : Control, INamingContainer
  {
    // Controls that make up the composite control
    Label lblUserName;
    TextBox txtUserName;
    Label lblPassword;
    TextBox txtPassword;
    Button btnAuthenticate;

    // Will be called by ASP.NET when it's time to render
    // the composite control
    protected override void CreateChildControls()
    {
      // Create the constituent controls
      lblUserName = new Label();
      txtUserName = new TextBox();
      lblPassword = new Label();
      txtPassword = new TextBox();
      btnAuthenticate = new Button();

      // Set properties
      lblUserName.Text = "User Name";
      lblPassword.Text = "Password";
      txtPassword.TextMode = TextBoxMode.Password;
      btnAuthenticate.Text = "Authenticate Me";

      // Add to the controls collection, together
      // with the necessary HTML goop
      Controls.Add(lblUserName);
      Controls.Add(new LiteralControl(" "));
      Controls.Add(txtUserName);
      Controls.Add(new LiteralControl("<br>"));
      Controls.Add(lblPassword);
      Controls.Add(new LiteralControl("&nbsp;"));
      Controls.Add(txtPassword);
      Controls.Add(new LiteralControl("<br>"));
      Controls.Add(btnAuthenticate);

      // Attach an event handler
      btnAuthenticate.Click += new EventHandler(btnAuthenticateClick);
    }

    // Handle the click event for the button
    public void btnAuthenticateClick(Object sender, EventArgs e)
    {
      string roles = null;

      // Build a roles string if we recognize the user
      if(txtUserName.Text == "Mike" && txtPassword.Text == "Soup")
      {
        roles = "User";
      }
      if(txtUserName.Text == "Adam" && txtPassword.Text == "Dinosaur")
      {
        roles = "Admin|User";
      }

      // If we didn't recognize the user, there will be no roles. In that
        // case, fall through and don't authenticate them
      if(roles != null)
      {
        // Create and tuck away the cookie
        FormsAuthenticationTicket authTicket = 
            new FormsAuthenticationTicket(1, txtUserName.Text,
          DateTime.Now, DateTime.Now.AddMinutes(15), false, roles);
        string encTicket = FormsAuthentication.Encrypt(authTicket);
        HttpCookie faCookie = new HttpCookie(
            FormsAuthentication.FormsCookieName, encTicket);
        Context.Response.Cookies.Add(faCookie);
        // And send the user where they were heading
        Context.Response.Redirect(FormsAuthentication.GetRedirectUrl(
            txtUserName.Text, false));
      }
    }
  }
}

The composite control code is really pretty simple. The key procedure here is CreateChildControls, which gets called when it's time to render the control at runtime. As you can see, I use this procedure to create the controls that make up my user interface, set some properties, and then add them to the Controls collection. Note the use of the LiteralControl class to insert some HTML markup between the individual textboxes and labels. The code that handles the button's click event is exactly the same code that I showed you last time. The difference is that this time the code is in the custom control instead of behind the web form.

Build the new custom control and it's ready to go.

Using the Custom Control

Related Reading

Programming ASP.NET
By Jesse Liberty, Dan Hurwitz

Using the custom control is just about as easy as creating it. To start, load up the ASP.NET application where you're using forms authentication, and delete all of the controls and code from the login.aspx page. Now decide which tab of the Toolbox should contain the custom control; I chose the Components tab, mainly because it's relatively uncluttered. Right-click on the tab and select Add/Remove Items. This will open the Customize Toolbox dialog box. Make sure the .NET Components tab is selected and click Browse. Locate your custom control's DLL file, and click Open and then OK. This will add the control to the Toolbox.

Now you can use the LoginControl just like any other control. In particular, you can drag and drop it to the login.aspx page. That's it! You don't have to write any code; it's all wrapped up in the control now, along with the user interface. You can run the project at this point to prove to yourself that the login logic is still working.

The ASP.NET Pipeline

Packaging the login user interface into a custom control is half the battle. But what about the code that's in global.asax. To make this code more easily reusable, it helps to know something about the ASP.NET pipeline. When a user requests an ASP.NET page from your server, the request isn't handled by one monolithic piece of software. Instead, it's passed from program to program along a virtual pipeline. Each program along the way can do its own bit of work with the request. Here are the components involved in the pipeline for a typical ASP.NET web form:

  1. IIS, which can provide its own authentication layer.
  2. aspnet_isapi.dll, which redirects requests for ASP.NET pages from IIS to the ASP.NET worker process.
  3. An instance of HttpApplication, which takes care of application-level processing. This is where global.asax comes into play.
  4. Zero or more HttpModule instances. These are extensions that handle various chores; for example, caching and state management are handled by system-wide HttpModule instances.
  5. The actual HttpHandler for the request. For web forms, this is normally an instance of the Page class.

For the problem at hand, the key point in this chain is the HttpModule. Any application can specify in its web.config file a set of HttpModule classes that requests should be routed through. By moving the code from global.asax to a custom HttpModule, I can make it more easily available to any application that wants to use it.

Creating the HttpModule

An HttpModule is nothing more than a class that implements IHttpModule. So to move my custom authentication out of global.asax, I created a new Class Library project with a single class named SetIdentity. Here's the code:


using System;
using System.Security.Principal;
using System.Web;
using System.Web.Security;
using System.Diagnostics;

namespace AuthModule
{
  public class SetIdentity: IHttpModule
  {
    public SetIdentity()
    {
    }

    public void Init(HttpApplication context)
    {
      context.AuthenticateRequest += 
          (new EventHandler(this.Application_AuthenticateRequest));
    }
    
    private void Application_AuthenticateRequest(Object source, EventArgs e)
    {
      HttpApplication application = (HttpApplication)source;
      HttpContext context = application.Context;

      // Get the authentication cookie
      string cookieName  = FormsAuthentication.FormsCookieName;
      HttpCookie authCookie  = context.Request.Cookies[cookieName];
      if(authCookie==null)
        return;

      // Get the authentication ticket 
      // and rebuild the principal & identity
      FormsAuthenticationTicket authTicket  = 
        FormsAuthentication.Decrypt(authCookie.Value);
      string[] roles = authTicket.UserData.Split(new Char [] {'|'});
      GenericIdentity userIdentity = new GenericIdentity(authTicket.Name);
      GenericPrincipal userPrincipal = 
          new GenericPrincipal(userIdentity, roles);
      context.User = userPrincipal;
    }

    public void Dispose()
    {
    }
  }
}

Most of the code here is unchanged from what I had in the global.asax file, but there are a couple of points to note. First, the HttpModule can hook into application-level events by adding its own set of event handlers in the Init procedure. That's how it happens that this HttpModule actually participates in the pipeline. Second, there's a bit of extra code in the actual event handler to derive the current context from the HttpApplication object that gets passed in.

To hook up the HttpModule class to an actual application, you need to do several things. First, of course, compile the code. Second, drop a copy of the dll into the application's bin folder. Finally, make an entry in the web.config file:


<configuration>
  <system.web>
    <httpModules>
      <add name="SetIdentity"
           type="AuthModule.SetIdentity, AuthModule" />
    </httpModules>
  ...
  </system.web>
</configuration>

With this change made, I can remove the remaining authentication code from the global.asax module. Now both halves of the process are in reusable components. To add authentication to any application, all I need to do is drop the login control on a form, copy the HttpModule library to the project, and make a few changes to the web.config file.

Working Hard for Laziness

If you step back to think about the entire journey, from no authentication at all to authentication wrapped up in reusable components, you'll see a pattern that appears over and over in software development. Faced with a problem (such as authenticating users to a web site), just about any developer can do the research to come up with a solution. The good developer will remember the solution the next time she's faced with a similar problem, and know which project contains the code that can be reused. The really good developer will spend the extra time to think about reuse explicitly, and to build as much of the solution as possible with an eye toward future reuse. By doing that little bit of extra work, you'll save a lot of effort in the future.

Mike Gunderloy is the lead developer for Larkware and author of numerous books and articles on programming topics.


Return to ONDotnet.com.

Copyright © 2009 O'Reilly Media, Inc.