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

Figure 1. The login user interface.
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(" "));
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.
|
Related Reading
Programming ASP.NET |
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.
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:
HttpApplication, which takes care of
application-level processing. This is where global.asax comes into
play.HttpModule instances. These are extensions that
handle various chores; for example, caching and state management are handled by
system-wide HttpModule instances.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.
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.
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.