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


Effective Forms Authentication, Part 1

by Mike Gunderloy
02/02/2004

ASP.NET offers several possibilities for authenticating users, but when you come right down to it, there's only one reasonable alternative for most applications: forms authentication. This is because Windows authentication requires every user to have an account in your Windows domain (which isn't reasonable, except for intranet applications), and Passport authentication requires you to pay quite a bit of money to Microsoft. Fortunately, forms authentication is both free and relatively easy to use. In this article (the first of two), I'll walk you through the basics, showing how you can use forms authentication in your own ASP.NET applications.

Who Am I?

What is "authentication," anyhow? Authentication is simply determining the identity of the user using a web application. If your web application authenticates its users, then it knows who is loading each page in the application. This information can be used for several purposes:

Whatever your use for authentication, your application will need to be able to retrieve the user's identity. To see how this works, build a web application consisting only of a single page, Default.aspx. Place a single Label control named lblIdentity on the page. Now add this code to the page's code-behind file:


void Page_Load(object sender, EventArgs e) {
  if(User.Identity.IsAuthenticated ) {
    lblIdentity.Text = "The current user is " + User.Identity.Name;
  }  else {
    lblIdentity.Text = "The current user is not authenticated.";
  }
}

The Page class in ASP.NET gives immediate access to the authenticated user. Alternatively, you can get this information from the Context. This is useful when using the authenticated user from component assemblies of a web application.


void SomeMethod() {
  if(HttpContext.Current.User.Identity.IsAuthenticated ) {
    Trace.Write("The current user is " + Context.User.Identity.Name);
  }  else {
    Trace.Write("The current user is not authenticated.");
  }
}

The Context object here is ASP.NET's intrinsic collection of various bits and pieces of information related to the current page. The User property returns an object that implements System.Security.Principal.IPrincipal. The IPrincipal interface represents a collection of security information about a user, including the user's identity and the roles to which the user belongs. From this, you can retrieve the corresponding IIdentity instance, which represents the user, and has properties to test for authentication and return the user's name.

If you run the project, you should get the result that the user is not authenticated. That's because by default, the Web is an anonymous place; if you don't do anything to implement your own authentication scheme, your application won't even try to authenticate users.

Programming ASP.NET

Related Reading

Programming ASP.NET
By Jesse Liberty, Dan Hurwitz

Adding Some Authentication

As you have probably deduced, forms authentication requires a form. Add a second web form, Login.aspx, to the project. For now, just place a single button, btnAuthenticate, on the form. Add a using statement for System.Web.Security at the top of the form's module, and then the code for this button to call:


private void btnAuthenticate_Click(object sender, System.EventArgs e)
{
  FormsAuthentication.RedirectFromLoginPage("Mike", false);
}

You'll also need to make some changes to the Web.config file to tell ASP.NET to use forms authentication with this particular application. First, replace the existing authentication section:


<authentication mode="Forms">
  <forms name="SampleAuth"
    loginUrl="Login.aspx" 
    slidingExpiration="true">
    </forms>
</authentication>
 

That configuration section tells ASP.NET to use forms authentication, and to create a cookie named SampleAuth to store information on the user's computer. The loginUrl attribute indicates the form that should be used for authentication, and the slidingExpiration attribute resets the expiration time of the cookie each time the application gets a request from the user (by default, sessions expire in 30 minutes).

You'll also need to modify the authorization section of the Web.config file:


<authorization>
    <deny users="?" />
</authorization>

By default, the authorization section in the Web.config file allows all users access to the entire application. With this change, it explicitly denies access for unauthenticated users.

Run the application again, and instead of Default.aspx, you'll see Login.aspx. Click the button, and the application will redirect you back to Default.aspx, where the label will show the name of the authenticated user. The sequence of events goes like this:

  1. The user requests Default.aspx.
  2. ASP.NET notes that the application uses forms authentication and that the user has not yet been authenticated, so it redirects to Login.aspx.
  3. When the user clicks the button, the code uses the FormsAuthentication.RedirectFromLoginPage method. This method announces to ASP.NET that you're satisfied with this user's credentials. The first parameter to the method is the name to assign to the user, and the second is a Boolean value that indicates whether a permanent cookie should be saved on the user's hard drive.
  4. ASP.NET knows that the user was originally trying to load Default.aspx, so that's the page that it redirects to, in this case.
  5. The identity of the now-authenticated user is available to any code that needs it.

Some of the Fiddly Bits

You can control the behavior of the forms authentication engine somewhat by the attributes that you supply for the <forms> element in the Web.config file. Here are the attributes that you can use for this element:

Attribute Purpose
loginUrl Name of the web form to send all unauthenticated requests to. This defaults to Default.aspx.
name Name of the cookie to use to store the authenticated identity. This defaults to .ASPXAUTH. If there are multiple applications on your server using forms authentication, and you leave this set to the default, then they'll all share the same cookie. That's a problem if they don't all have the same authentication requirements. I recommend always setting this to a unique value for each application.
path Path where the cookie will be stored. Defaults to a single slash.
protection Specifies the verification method to use to prevent cookie spoofing: None, Encryption (encrypts but doesn't validate the cookie), Validation (adds a validation key to the cookie), and All (the default, which both encrypts and validates the cookie). Unless your web server is extremely overloaded, leave this at the default value of All.
requiresSSL A Boolean value that determines whether ASP.NET demands an SSL connection for the cookie. The default is false.
slidingExpiration Specifies whether the cookie times out at a fixed point after the initial authentication (false, which is the default), or whether the timeout is reset with each request (true).
timeout The number of minutes it takes for the cookie to expire. The default is 30. For permanent cookies, this attribute is ignored.

A Little More Realism, Please

So far, my forms-based authentication isn't really doing much authenticating; anyone who can click a button has access to the entire application. It's up to you to decide just how you want to authenticate users of your application, and to supply custom code to fit your scheme. A typical scheme might require users to type in a username and a password on the login page. To support this, add two TextBox controls, txtUsername and txtPassword, to the Login.aspx page. Now you can alter the code to distinguish between different users:


void btnAuthenticate_Click(object sender, EventArgs e) {
  if(txtUserName.Text == "Mike" && 
     txtPassword.Text == "Soup") {
    FormsAuthentication.RedirectFromLoginPage(txtUserName.Text, 
                                              false);
  }
  
  if(txtUserName.Text == "Adam" && 
     txtPassword.Text == "Dinosaur") {
    FormsAuthentication.RedirectFromLoginPage(txtUserName.Text, 
                                              false);
  }
}

Now you'll find that if you submit a proper combination of username and password, you still end up on Default.aspx, and it knows which user you logged in as. If you submit an invalid combination, the call to FormsAuthentication.RedirectFromLoginPage will never happen, and the browser will just reload Login.aspx. Of course, hard-wiring usernames and passwords directly into your source code is a pretty horrible idea; it means you have to modify the source code every time your user list changes. In a real application, you'd typically check against a database or XML file of users, which can be modified more easily at runtime. But the authentication engine doesn't care; you can use whatever scheme you like to decide whether users are legitimate.

Role-Based Security

At this point, you can check a user's exact identity by looking at the appropriate IIdentity object and using that identity to authorize actions:


if(Context.User.Identity.Name == "Adam") {
  // allow functionality for user Adam
}

But this bit of code is another maintainability trap. Rather than check a user's identity, you're usually better off checking a user's membership in a particular role. For example, if you're authorizing access to administrative functions, create an Admin role and place administrative users into the role. Then you can check for role membership in your code:


if(Context.User.IsInRole("Admin")) {
  // allow administrative functions
}

With this code for authorizing functionality, you can maintain your list of users and roles externally (in a database or XML file, as I mentioned earlier) and never need to change your code unless the administrative functionality itself changes. But how to implement this? One option is to create your own Principal and Identity objects. The System.Security.Principal namespace contains GenericPrincipal and GenericIdentity objects that you can use anywhere the IPrincipal or IIdentity interfaces are required. Armed with these objects, you might rewrite the login code again:


void btnAuthenticate_Click(object sender, EventArgs e) {
  if(txtUserName.Text == "Mike" && 
     txtPassword.Text == "Soup") {
    string[] roles = {"User"};
    DoAuthenticate(txtUserName.Text, roles);
  }
  
  if(txtUserName.Text == "Adam" && 
     txtPassword.Text == "Dinosaur") {
    string[] roles = {"Admin", "User"};
    DoAuthenticate(txtUserName.Text, roles);
  }

  if(Context.User.Identity.IsAuthenticated ) {
    FormsAuthentication.RedirectFromLoginPage(txtUserName.Text, 
                                              false);
  }
}

private void DoAuthenticate (string userName, string[] roles) {
  GenericIdentity userIdentity = new GenericIdentity(userName);
  GenericPrincipal userPrincipal = 
    new GenericPrincipal(userIdentity, roles);
  Context.User = userPrincipal;
}

The logic here is straightforward: if the username and password are acceptable, build a string array of role names and use that together with a GenericIdentity object to build a GenericPrincipal. Assign that to the User maintained by the ASP.NET Context, and all should be well. To check, I can add a little extra code to the Default.aspx page:


if(Context.User.IsInRole("Admin")) {
  lblIdentity.Text += " (Admin)";
}

The Fly in the Ointment

There's only one minor problem with this scheme: it doesn't work. Oh, users will still be able to log in and authenticate. But even if you supply the username and password of a user in the Admin role, the Default.aspx page will refuse to recognize them as an Admin user.

What's going on? Figure 1 tells the tale. The Autos window here shows the state of the variables with execution paused on the Default.aspx page. You'll note that the GenericPrincipal object has no roles at all. That's because forms authentication by itself doesn't bother to save the Principal information I assigned; it only saves the Identity information.

RedirectFromLoginPage builds its own GenericPrincipal
Figure 1: RedirectFromLoginPage builds its own GenericPrincipal

It's tempting to think that something really wacky is going on here. After all, I just assigned a perfectly good GenericPrincipal object to the current context. What happened to it? The answer is that this is a web application, not a Windows application. It's not like there's a Context object that hangs around, waiting for me to get back to it. With every page request, the Context gets built again. That's just how a stateless application works.

When you tell ASP.NET to use forms authentication, it checks to see whether a particular cookie is present with each request. Calling the RedirectFromLoginPage method creates this cookie and drops the specified identity into it. When ASP.NET goes to authenticate a user, it checks for this cookie and rebuilds the identity if it finds the cookie. The designers just didn't include the principal in this scheme.

So Now What?

The idea of assigning your own Principal and Identity objects is a good one. It's just the execution that's at fault here. Rather than build the custom security objects before the forms authentication has done its work, the key is to add the principal information to the cookie, and make use of it later.

The first half of this requires rewriting the login code yet again. Instead of actually constructing a GenericPrincipal object, I'll just tell the forms authentication engine to tuck a list of groups away in its cookie:


void btnAuthenticate_Click(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);
    Response.Cookies.Add(faCookie);
    
    // And send the user where they were heading
    string redirectUrl = 
      FormsAuthentication.GetRedirectUrl(txtUserName.Text, false);
    Response.Redirect(redirectUrl);
  }
}

Now, when the code recognizes a valid user, it still builds a list of roles, but this time as a single string with individual roles separated by a pipe (|) character. It then creates an object called a FormsAuthenticationTicket. This is the class that contains all of the information that the forms authentication module saves to its cookies. The constructor I've used takes six arguments:

  1. A version number for the ticket.
  2. The user name to use with the Identity object.
  3. The time the ticket was created.
  4. The time the ticket should expire.
  5. A Boolean indicating whether the ticket is persistent.
  6. An arbitrary string of user-defined data.

The code now tucks the list of roles away in the user-defined data parameter. It then encrypts the ticket for security, creates a cookie containing this information, and adds the cookie to the response stream. The final step is to redirect the user to the page where they were trying to go in the first place, which is retrieved with the GetRedirectUrl method. There's no need to call RedirectFromLoginPage, because I'm creating the cookie by hand instead of depending on forms authentication to do it for me.

Building the Principal

The remaining step is to use the information that's saved in the authentication cookie to build the principal and identity that I want for this user. For this, I can turn to the global.asax file. As you probably already know, this file contains code to respond to certain application- and page-level events in ASP.NET. In particular, you can hook the Application.AuthenticateRequest event, which is called every time ASP.NET wants to find out whether the current user is authenticated. With forms authentication in place, that will happen on every page request. Here's the code:


protected void Application_AuthenticateRequest(Object sender, 
                                               EventArgs e) {

  // Get the authentication cookie
  string cookieName  = FormsAuthentication.FormsCookieName;
  HttpCookie authCookie  = Context.Request.Cookies[cookieName];
  
  // If the cookie can't be found, don't issue the ticket
  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;
}

This handler first retrieves the authentication cookie; if there isn't any such cookie, then it just exits, which will cause the forms authentication module to kick in and load the login page. Otherwise, it reads the user data back out of the ticket, and uses that, along with the standard information saved by forms authentication, to build the proper principal and identity, finally assigning them to the current context.

With this change, things work properly for a web application. If you check with the change to Default.aspx that I put in earlier, you'll find that the Admin user is now properly recognized.

What Next?

By now you should have a pretty good handle on how forms authentication works. But there's still room for improvement in this solution. Imagine you had five, or ten, or fifty ASP.NET applications that all shared this same basic authentication scheme. Seems a shame to repeat all that code, doesn't it? In my next article, I'll show you how to more easily encapsulate some of these pieces for reuse.

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.