Security

Security Headaches? Take ASP.NET 2.0!

Keith Brown

This article is based on the March 2004 Community Technology Preview of ASP.NET 2.0. All information contained herein is subject to change.

This article discusses:
  • Security enhancements in ASP.NET 2.0
  • Server-side security controls
  • User and role databases
  • Cookieless forms authentication
This article uses the following technologies:
ASP.NET, Authentication

Contents

Forms Authentication Gets a Boost
Getting Started
Server-side Security Controls
Defining Roles
Password Recovery
Provider Tweaks
Control Tweaks
Programming Membership and Roles
Cookieless Forms Authentication
An Ounce of Prevention
Conclusion

New security features are an important improvement in ASP.NET 2.0. These features include membership services that manage a database of user accounts, hashed passwords, a role manager for managing role membership for users, and five new server-side controls that make implementing forms authentication much easier. ASP.NET 2.0 also offers a provider model that gives you complete control over the implementation of the Membership and Role services and cookieless forms authentication. You also get easy Web-based administration for simple local and remote administration of user accounts and roles, as well as enhanced control of other non-security related settings.

Forms Authentication Gets a Boost

Forms authentication is one of the most popular features of ASP.NET 1.0 because it encapsulates best practices that were missing from many ad hoc implementations. For example, how many forms authentication implementations do you know of that protect the integrity of the cookie used to hold the client's credentials? Forms authentication not only writes the user's name into the cookie, it adds a message authentication code (a hash formed from the cookie and a secret value that only the Web server knows). This prevents a malicious client from elevating privileges or viewing another user's data by changing the name in her cookie.

If you follow the various newsgroups and list servers where .NET Web developers hang out, you'll read of people implementing the same things over and over: user databases, roles cached in cookies, controls for capturing user names and passwords, and administration tools for managing users and roles. The ASP.NET team has provided built-in solutions to virtually all of these problems. In researching the alpha build of ASP.NET 2.0, I was blown away by the sheer reduction in the amount of code that it takes to build a Web site that uses forms authentication in a manageable way.

Getting Started

You'll see how easy it is to get started using these new features as I walk you through some experiments that you can perform if you have the ASP.NET 2.0 bits, which is available to MSDN® Universal subscribers as a download.

To start, you'll need a virtual directory that points to an empty directory. You must make sure that your ASP.NET worker process has permissions to read, execute, and write to it. If you're running Windows® 2000 or Windows XP, you need to grant these permissions to the ASPNET local account, whereas under Windows Server™ 2003 you need to grant permissions to the Network Service account instead.

I'm going to be working with forms authentication, so I'll need to enable it with a web.config file. Now if I were showing you how to use ASP.NET 1.1, I'd tell you to open a text editor and start typing the XML by hand. But one of my favorite features in ASP.NET 2.0 is the interactive configuration file editor that's built right into the IIS management console, which you'll find on the "ASP.NET" tab on the property sheet for your virtual directory. Press the "Edit configuration" button to bring up the editor.

Figure 1 Configuration Editor

Figure 1** Configuration Editor **

Figure 1 shows this new editor. You see I've chosen forms authentication instead of the default: Windows authentication. Do this for your own virtual directory. While you're in the configuration tool, set the default language for your Web app to C# since it'll save you some typing later. The Page Language Default setting is the first dropdown on the Application tab. After applying these changes, you'll find a web.config file in your directory with all your settings.

You'll need to register some users with the Membership service to get started, so the first page you'll be writing is one that allows you to add users. Here's a server control slated for the beta which allows you to implement this page in three lines of code:

<form runat='server'>
  <asp:createuser runat='server'/>
</form>

Since I'm using the alpha bits, though, I'll have to code this particular form by hand using the Membership class directly. For now, just use the ASPX page shown in Figure 2, and I'll discuss the Membership class later in the article. Figure 3 shows what you'll see when you point your browser to this page. Go ahead and add some users and passwords now. Your job should be much easier since this works right out of the box!

Figure 2 adduser.aspx

<form runat='server'>
<table>
<tr><td>Name:</td><td><asp:textbox id='name' runat='server'/></td></tr>
<tr><td>Email address:</td><td><asp:textbox
  id='email' runat='server'/></td></tr>
<tr><td>Password:</td><td><asp:textbox textmode='Password'
  id='pwd1' runat='server'/></td></tr>
<tr><td>Retype password:</td><td><asp:textbox textmode='Password'
  id='pwd2' runat='server'/></td></tr>
</table>
<asp:button text='Submit' onclick='onSubmit' runat='server'/>
<p>
<asp:label id='msg' runat='server'/>
</form>

<script runat='server'>
void onSubmit(object sender, EventArgs args) {
  if (pwd1.Text.Equals(pwd2.Text)) {
    MembershipCreateStatus status;
    MembershipUser newUser = Membership.CreateUser(name.Text,
      pwd1.Text, email.Text, out status);
    msg.Text = status.ToString();
  }
  else msg.Text = "Passwords don't match, try again.";
}
</script>

Figure 3 Membership Page

Figure 3** Membership Page **

Once you're finished adding users, take a close look at your virtual directory. You should see a new subdirectory called "DATA" that has a Microsoft® Access database inside. This is where the Membership and Role services store their data by default, but later I'll show how you can override the default storage mechanism to use SQL Server™ or your own custom data repository. Now it's time to use the security controls that are available in ASP.NET 2.0.

Server-side Security Controls

Figure 4 lists five new security controls available in ASP.NET 2.0. A good place to start exploring is the LoginStatus control. Start by creating a new ASPX page with this control on it. Call the new page default.aspx for simplicity:

<form runat='server'>
  <asp:loginstatus runat='server'/>
</form>

Figure 4 New Security Controls in ASP.NET 2.0

Control Description
LoginStatus Provides a button for logging in or logging out, depending on the user's current state
Login Provides a form for collecting and validating login credentials against the Membership database
LoginName Displays the name of the logged-in user
LoginView Displays alternate content depending on whether the user is logged in or not, and optionally based on the roles in which the user is a member
PasswordRecovery Provides a form for requesting a forgotten password

Point a browser to this page and you should see a Login link. If you view the source of the resulting page in your browser, you'll see that this hyperlink points to a page called login.aspx, which you've not yet written. This is yet another three-line Web page, so go ahead and create it now:

<form runat='server'>
  <asp:login runat='server'/>
</form>

If you've ever implemented forms authentication by hand, you'll appreciate these three lines of code. In the past, an equivalent implementation to perform a database lookup would have required a couple orders of magnitude more code.

Now go back to your browser and click the Login link, which should take you to the login page shown in Figure 5. Try logging in with an invalid user name or password, and notice that an appropriate default error message appears. The message doesn't give an attacker too much information. There's no opportunity for a naive developer to inadvertently send a message back to the user telling him he got the user name right, please try guessing another password!

Figure 5 Login Page

Figure 5** Login Page **

Go ahead and type in a valid user name and password—one that you entered earlier through the adduser.aspx page—and you should be redirected back to the default.aspx page. Since you didn't supply any custom action to the login control, by default it simply logs you in using forms authentication, which means your browser now has an encrypted cookie holding your user name.

Now that you've been redirected back to default.aspx, do you see anything different? The login status control should now say Logout instead of Login. Because the forms authentication cookie was sent with the request, the FormsAuthenticationModule has created an authenticated user principal and associated it with the context of the request. The login status control notices this and changes to allow you to log out. Try logging out and logging back in to see this work.

Now let's add a bit more code to the default.aspx page:

<h3>User Name: <%= User.Identity.Name %>
</h3>
<h3>User Type: <%= User.GetType() %></h3>

Refresh the page and you should see the user name that you logged in with. Notice that the underlying object representing the user is of type GenericPrincipal, which is how FormsAuthenticationModule represents users. Once you turn on the Role Manager you'll see this type change, because when enabled, the new RoleManagerModule replaces the principal generated by FormsAuthentication with its own type.

Now let's add a LoginView control to default.aspx that displays alternate content depending on the user's login. The simplest way to use this control is to provide two blocks of content: one for an anonymous request (before the user has logged in), and another one for an authenticated request (after the user has logged in):

<asp:loginview runat='server'>
  <anonymoustemplate>
    <h4>If you see this, you've not yet logged in!</h4>
  </anonymoustemplate>
  <loggedintemplate>
    <h4>Welcome to my website, <asp:loginname runat='server'/>!</h4>
  </loggedintemplate>
</asp:loginview>

As you log in and log out, you should see the text in the LoginView control change as expected. It's a very simple idea, but it sure does make your code a whole lot clearer.

Defining Roles

I've crafted a simple page that allows you to add users to roles using the Role Manager, but before you can use it you'll need to enable the Role Manager for your app. Go back to the configuration tool and find the Authentication tab. Check the box that says "Role management enabled" and apply the change.

The code for the addrole.aspx page is shown in Figure 6, while Figure 7 shows what the form looks like. Put this page in your virtual directory and point your browser at it so that you can add some roles. Specify a user name (one that you added previously with the adduser.aspx form) along with a role name, and press the button to add the user to that role. The code first adds the role if it doesn't yet exist, then adds the user to the role. Under the covers, the Role Manager tracks these role mappings in the same Microsoft Access database that the Membership service used, but that's really just a coincidence. The Role Manager can store its data in SQL Server or any other store, and it doesn't have to use the same mechanism as the Membership service. The provider models for Membership and Role Manager are distinct in order to allow this.

Figure 6 addrole.aspx

<form runat='server'>
<table>
<tr><td>Role:</td><td><asp:textbox id='role' runat='server'/></td></tr>
<tr><td>User:</td><td><asp:textbox id='user' runat='server'/></td></tr>
</table>
<asp:button text='Add user to role!' onclick='onSubmit' runat='server'/>
<p>
<asp:label id='msg' runat='server' viewstateenabled='false'/>
</form>

<script runat='server'>
void onSubmit(object sender, EventArgs args) {
  if (!Roles.RoleExists(role.Text)) {
    Roles.CreateRole(role.Text);
    msg.Text = "Created a new role.";
  }
  Roles.AddUserToRole(user.Text, role.Text);
}
</script>

Figure 7 Add Role

Figure 7** Add Role **

If you've ever implemented custom roles in ASP.NET, you'll appreciate the built-in Role Manager for you no longer have to be a master of the ASP.NET HTTP pipeline in order to implement role-based security. Once you've added some roles, you can go back to default.aspx and have some fun with the LoginView control. Add another section after the <loggedintemplate/> element:

<rolegroups>
  <asp:rolegroup roles='ForumModerators'>
    <contenttemplate>
      <h4>Controls for forum moderators
          go here.</h4>
    </contenttemplate>
  </asp:rolegroup>
  <asp:rolegroup roles='Friends'>
    <contenttemplate>
      <h4>Welcome, friend!</h4>
    </contenttemplate>
  </asp:rolegroup>
</rolegroups>

You probably didn't choose the same roles that I did, so you'll need to replace my role names with your own, and tweak the content so it's appropriate for the role. Once you're finished, test out your new page by logging in under different user accounts in different roles, and watch how the content of the page changes as the roles change. Note that if two rolegroups match the user's role, the first one to match (from top to bottom) will always be displayed.

Although it's not new, remember that you can always test roles programmatically through User.IsInRole. Also bear in mind that you can use the <authorization/> section in web.config to grant or deny access to various pages, like so:

<authorization>
  <deny users='?'/>
  <allow roles='ForumModerators'/>
  <deny users='*'/>
</authorization>

The first entry tells ASP.NET to bounce any unauthenticated requests (force authentication). The second and third entries ensure that only ForumModerators can access the content in the directory tree where this web.config file resides. Remember that the authorization section can be used in web.config files placed in subdirectories, and can also be used in a <location/> element to control access to individual files.

Password Recovery

I haven't shown you the password recovery control in this introductory demo because its use requires careful consideration. You probably know what this control does: it lets the user request that his password be e-mailed to him. You need to do some risk assessment before deciding to e-mail cleartext passwords to users.

In fact, if you were to plunk down this control on a page in your existing site, it wouldn't operate because the Membership service refuses to disclose cleartext passwords by default. It couldn't even do so if it wanted to because by default it only stores a one-way hash of the password, not the password itself. When asked to validate a password, the Membership service hashes the presented password, then compares the hash value with its copy. If you want to recover cleartext passwords, you can reconfigure the Membership provider to store the passwords in encrypted form, in which case the provider will use the <machineKey/> to encrypt the password. Thus it can be decrypted and e-mailed to the user.

If you store hashed passwords, which is a really good idea, you need to prepare an alternate way of authenticating the user. You cannot e-mail a user his password, but if you've asked some questions ahead of time, such as "What is your favorite pet's name?" you can use the answers to authenticate the user and allow him to send you a new password. While the Membership service does support keeping a question and answer for each user, it's used only to decide whether to e-mail the password, so it can't be used with hashed passwords. This area could use some work, in my opinion.

A good model for password resets via question and answer was proposed by Viega and McGraw on page 95 of Building Secure Software (Addison-Wesley, 2002). It entails using a collection of hundreds of questions, and randomly picking a set of questions to ask the user when she first sets up her account. You can then ask the user a subset of these questions if she requests a password reset. This requires her to answer a number of the challenge questions correctly to proceed. If the user is successful, you choose a new set of random questions to ask to replace the questions previously used.

Provider Tweaks

Thus far, I purposely used default settings to keep it simple, but you'll need to tweak these settings for your own environment. For example, if you want the Membership service to store its data in SQL Server, you should select the AspNetSqlProvider instead of AspNetAccessProvider—the default. This setting is on the Authentication page of the configuration tool.

But what if you already have an existing user database that you need to integrate? It certainly won't have the tables and columns that the AspNetSqlProvider needs. Furthermore, what if it's on an AS/400 server or an Oracle installation? Fortunately, both the Membership and Role Manager systems are built on a layered model that I've shown in Figure 8. You can completely replace the Membership data store by extending the abstract MembershipProvider class, which is defined in the System.Web.Security namespace. Similarly, you can replace the Role Manager data store by extending RoleProvider. Rob Howard discusses the provider model in greater detail in his "'Nothin' But ASP.NET" column.

Figure 8 Provider Model

Figure 8** Provider Model **

Certainly it's easiest to use the existing providers. In the alpha version, there are two. One works with an Access database and, as you've seen, it works fine right out of the box. The second is the SQL Server provider that I mentioned earlier. By the beta release there should also be a Membership provider that validates users against Active Directory®, and a Role provider that looks up roles from Authorization Manager.

Even if you choose one of the built-in providers, you can tweak its behavior in web.config. Figure 9 shows the provider settings for the SQL Server Membership provider. Note the passwordFormat setting where you can choose from three options: Hashed (the default), Encrypted, and Clear. Then you can choose your policy for password retrieval with the enablePasswordRetrieval and requiresQuestionAndAnswer attributes. Of course, if you choose to use hashed passwords, you'll have to set enablePasswordRetrieval to false. Otherwise, you can require the user to answer a challenge question before the system will e-mail his password.

Figure 9 Provider Settings

Figure 9** Provider Settings **

The connection string for the database isn't stored in your web.config file; rather, it's referenced indirectly. Note that the property is called connectionStringName and points to a machine.config section specifically designed to hold connection strings. Keeping connection strings out of your web.config file is a good idea, especially if you can't use integrated authentication and are forced to use passwords. ASP.NET 2.0 is slated to support XML encryption for sensitive sections of configuration files—a really handy feature for the connection string section in machine.config.

The Role Manager can be configured to use cookies or URL munging and can cache roles in the cookie to reduce round-trips to the role database. The caching is intelligent: if the number of cached roles starts to get too large, the Role Manager will cache the most recently used roles in the cookie and dynamically look up the least-used roles. This feature was probably motivated by the need to support mobile devices with limited storage capacity.

There are a number of other settings you can tweak, but I'll leave you to explore them on your own. In the meantime, let's look at ways you can tweak the server-side security controls used earlier.

Control Tweaks

It's neat to be able to create a login page with three lines of code, but generally speaking, you'll want to customize the login control a bit to suit your application. Figure 10 shows some code you can use to replace the simple login page created earlier. In addition, the appearance of these controls can be modified with all the properties you'd expect on a Web control. And with the themes support in ASP.NET 2.0, you can maintain a consistent appearance across your entire Web site without having to change your code.

Figure 10 Customizing Login Control

<form runat='server'>
  <asp:login runat='server'
    titletext='ACME.COM Security'
    instructiontext='You must log in to continue'
    usernamelabeltext='Email:'
    submitbuttontext='Continue'
    createusertext='register'
    createuserurl='adduser.aspx'
    displayrememberme='false'
    failuretext='Leave now, hacker!'
    helppagetext='help'
    helppageurl='https://www.success.com'
  />
</form>

One interesting feature of the login control is that it doesn't have to be stuck on its own page as I've done in this example. Rather, you can make it part of your master page, so it always shows up in the margin. Once the user logs in, you don't really want to see it anymore, so by default it disappears when it detects that an authenticated user is already present. You can tweak this behavior with the VisibleWhenLoggedIn property. This is an example of the features developers have been implementing manually with ASP.NET 1.1, and which are now built into ASP.NET 2.0.

The other controls have similar options. For example, if you would rather display a pretty button for the user to log in or out, set the Login(Out)ImageUrl properties on the login status control.

To get a feel for how it all works, use the Visual Studio® 2005 project wizard to create an Internet Web site. As of this writing, this wizard only shows up if you've imported the "Web.vssettings" IDE settings file into Visual Studio. You can do this via the Tools-Import/Export Settings dialog. The wizard includes all of the features I've talked about so far and allows plenty of UI customization to get the look and functionality you crave in your new Web site.

Programming Membership and Roles

While you're likely to get a lot of mileage out of the server-side security controls, it's good to know that the classes used to implement this high-level functionality are also available to you directly. There are two main classes you'll want to examine to learn the programming model for these services: Membership and Roles. I don't have room for an exhaustive treatment of them here and the details are sure to change as the product moves toward a final release, but let me give you a taste of what you'll find.

From the Membership class, you can create and manage users, each of whom is represented by an instance of the MembershipUser class. This class represents a user profile that includes properties such as Email, CreationDate, PasswordQuestion, and so on. When you create and update these user profiles, you do so via the Membership class which, due to its layered model, hides the details of where and how the profiles are stored (see Figure 8). Methods for changing the user's password and resetting the password to a random, computer-generated password are provided on this class, as is a timestamp that tracks the user's activity in order to maintain a count of current users (you can get this number by calling the GetNumberOfUsersOnline method on the Membership class).

To validate a user's password, just call ValidateUser on the Membership class, passing in the user name and password. The underlying provider will take care of all necessary password hashing or decryption. If a user forgets his user name, you can remind him by asking for an e-mail address and passing it to GetUserNameByEmail but that's not a secure option.

Cookieless Forms Authentication

One of the biggest complaints I get when I teach ASP.NET forms authentication is that it requires cookies. Fortunately, this restriction is gone in ASP.NET 2.0. There's a new "cookieless" attribute on the <forms/> element in web.config. You can set this to one of four values: UseCookies, UseUri, UseDeviceProfile, or AutoDetect.

UseCookies and UseUri force FormsAuthenticationModule to either use cookies or URL munging, respectively, for all requests. UseDeviceProfile says to look at the browser capabilities to determine which mode to use. Finally, AutoDetect will attempt to set a cookie and, if it fails, will use URL munging instead. Here's what a typical URL will look like once it's munged (the ellipses are mine since these URLs can get very long): https://www.acme.com/foo/(F(Cvc...A1))/default.aspx.

The parenthesized section of the URL contains the data the cookie would normally contain, and is stripped off by a module in the HTTP pipeline, so if you read the Request.Path property from an ASPX page, you won't see any of that extra goo in the URL. If you redirect the request, the URL will be munged automatically. In other words, this code will bring you (correctly) back to the same page you're currently looking at, with the URL correctly munged:

Response.Redirect(Request.Path)

This feature should make forms authentication considerably more widely implemented. However, as more Web sites use ASP.NET forms authentication, more attackers will attempt to find weaknesses, so it's a good idea to follow some ground rules.

An Ounce of Prevention

Forms authentication isn't very strong if it's not protected by secure sockets layer (SSL). At the very least, your login page should be sent to the user and shipped back to the Web server over a secure connection to prevent an eavesdropper from sniffing the user's cleartext password. But that's usually not enough. An eavesdropper who steals a forms authentication cookie has stolen the login because due to the way cookies work, there's no way to implement replay detection. Remember that cookies are normally sent with every request, even for things as simple as a request for a GIF file for a button on the page. Once stolen, an attacker can use the cookie to impersonate the user. To mitigate this risk you either need to shorten the cookie timeout drastically or run entire sections of the Web site (or better yet, the whole darn thing) over SSL.

I prefer the latter approach for sites that require high security, and when people complain that SSL is slow, I ask them why they haven't bought hardware to accelerate it. Some companies will insist on using SSL for only part of the site, however. If this is your situation, you can mitigate these cookie replay attacks by turning on the requireSSL attribute in the <forms/> element. This adds the "Secure" attribute to the forms authentication cookie, which indicates that the browser should only send the cookie back to the server over a secure channel. In other words, it won't be sent with requests that don't run over SSL. This feature was added in version 1.1 of the .NET Framework so it's not unique to ASP.NET 2.0. What is new in ASP.NET 2.0 is that this countermeasure can also be applied to session cookies:

<httpCookies requireSSL='true' httpOnlyCookies='true'/>

Since secure cookies won't be sent with requests that don't come over SSL, for pages that are accessible via raw HTTP, you can bet that User.Identity.IsAuthenticated will return false every time. In other words, you won't know who the user is on any pages that don't run over SSL. Note that even if you decide to run your entire site over SSL, it's a really good idea to turn on requireSSL, in case you accidentally allow access to a file or two over raw HTTP.

The httpOnlyCookies attribute is useful as a measure against cross-site scripting attacks; it indicates to the browser that the cookie should not be accessible from scripts. This uses a cookie attribute called HttpOnly that is currently only recognized by modern versions of Internet Explorer, but it's a great idea that I hope other browser vendors will adopt. To learn more, see Some Bad News and Some Good News.

Conclusion

ASP.NET 2.0 provides significant security advantages on Web sites that use forms authentication. By providing a user profile repository with support for roles, forms authentication will move beyond the purview of the ASP.NET internals guru to become much more widely implemented. It's going to be tough for me to go back and use the old stuff now!

Keith Brown is an independent consultant specializing in application security. Keith regularly teaches for DevelopMentor, where he oversees the security curriculum. He authored Programming Windows Security (Addison-Wesley, 2000) and is writing a new security book on .NET. Read it online at https://www.pluralsight.com/keith.