Making Sense: ASP.NET Security

Download Source Code

A common task for ASP.NET developers is to implement security on a site. This includes
log-in, registration, password management, and user management. The following sections
explain how to accomplish all of these tasks, showing some of the limitations of
out-of-the-box controls and workarounds that you can use in your application. While
there are many features and options in ASP.NET security, a single article can’t
cover them all. Therefore, I’ll discuss the most typical scenarios that you’ll use
in practice.

Setting Up

To get started, you’ll need to create a Web site and configure your database with
ASP.NET tables. The following sections walk you through the tools that help you
do this.

Create a Website

For this article, we’ll be using an ASP.NET Website. The same techniques for an
ASP.NET Website apply to an ASP.NET Application, but a discussion of the project
types is out of scope for this article. To create the Website, open Visual Studio
2008 (VS 2008) and select File -> New -> Website, which shows the New Web
Site window in Figure 1.

Figure 1. Creating a New Website

Creating a New Website

As shown in Figure 1, I based my Web site off the File System, am using C#
as the language, and specified the Web site name as AspDotNetSecurity. Click
the OK button to continue and VS 2008 will create a new solution, shown in
Figure 2.

Figure 2. A New ASP.NET Website

A New ASP.NET Website

Figure 2 shows that all you have is a Default.aspx page, a web.config
file, and an App_Data folder to start with. To keep this article as simple
as possible, we won’t be using Master pages or any other ASP.NET features. However,
any time you begin a real site, you’ll want to set up Master pages first so that
your security controls appear where you want.

Note: It’s possible that your screen might not look like mine, from Figure
2, since my VS 2008 environment is set to Visual C# settings and yours might be
set to something else. If you want your screen to look like mine, delete this project
and then select Tools -> Import and Export Settings and step through the wizard
to reset your settings to Visual C#. Then re-create your ASP.NET Website, as described
in previous paragraphs.

Now that you have a Website, you’ll need a database to hold security information.

Database Configuration

The first thing you’ll need to do is create a database to hold your security information.
The database must be SQL Server. Any version of SQL Server from 2000 and later will
work. You could create the database via SQL Server tools or VS 2008. If you don’t
have SQL Server, perhaps because you are using only the .NET Framework SDK or an
express version of Visual Studio, you can download
SQL Server 2008 Express
for free. For this article, I’ll create a database
for through VS 2008 (not an express edition).

To create the database, with VS 2008, open the Server Explorer by selecting View
-> Server Explorer. Perform a right-click on Data Connections, in Server Explorer,
and select Create New SQL Server Database, shown in Figure 3. Click the OK button
to create the database.

Figure 3. Creating a new Database

Creating a New Database

Next, you’ll set up the proper security objects in your database. ASP.NET ships
with a utility named aspnet_regsql.exe that will automatically set up your
database with necessary security objects. This utility will install tables for other
ASP.NET features, such as profiles and health management, but those subjects are
out of scope for this article. You can find this utility at %windir%\Microsoft.NET\Framework\v2.0.50727.
Running the aspnet_regsql.exe utility, you’ll see the window in Figure 4.

Note: You might think it’s strange to use utilities in the .NET Framework
2.0 folder if you’re working with a later version of ASP.NET. However, later versions
such as ASP.NET v3.5 still use the .NET 2.0 CLR.

Figure 4. ASP.NET SQL Server Setup Wizard Welcome Page

SQL Server Setup Wizard Welcome Screen

As you can see in Figure 4, the Welcome page explains what the wizard does. Click
Next to select a Start Up Option, shown in Figure 5.

Figure 5. ASP.NET SQL Server Setup Wizard Start Up Options

SQL Server Setup Startup Options

In the Start Up Options, Figure 5, select Configure SQL Server for application services
and click Next. You’ll see the window for Server and Database selection in
Figure 6.

Figure 6. ASP.NET SQL Server Setup Wizard Server and Database Selection

SQL Server Setup Server and Database Options

Select the same server and database as configured earlier, Figure 3, and click Next.
You’ll see a summary page and can click the Next button to configure the database.
The next window you’ll see is a confirmation that the database has been set up.
You can click the Finish button to close the wizard. Your database now contains
many new objects, Figure 7.

Figure 7. Database Objects Created by the ASP.NET SQL Server Setup Wizard

Database Objects Created by the ASP.NET SQL Server Wizard

You generally don’t need to know the contents and schema of the objects shown in
Figure 7, but should be aware of their presence. You can see that tables and stored
procedures have aspnet_ prefixes and views have vw_aspnet_ prefixes.
You should never delete these objects from your database. Finally, add the following
connection string for the new database to your web.config file, which will replace
the default <connectionStrings /> entry:

  <connectionStrings>
 <clear/>
 <add name="LocalSqlServer" connectionString="Data Source=.;Initial Catalog=AspDotNetSecurity;Integrated Security=True;Pooling=False"/>
 </connectionStrings>

The reason I named the connection string LocalSqlServer is because that is
the name of the default connection string from machine.config that points to a local
SQL Express database. Further, all of the provider configurations (membership, roles,
etc.) in machine.config refer to LocalSqlServer as their connection string.
Adding the clear element and then the add element removes the machine.config
connection string and replaces it with this application’s connection string in the
scope of this application only. Therefore, the default provider definitions use
the connection string for the database that I’ve configured for this Web site.

Tip: To prevent mistyping the connection string, you can right-click on the
database in Server Explorer, select Properties from the context menu, and then copy-and-paste
the Connection String property in the Properties window.

With a Website and database in place, you’re ready to begin adding security to your
site. I’ll be using the ASP.NET Configuration Tool to set up security. You can start
this tool by selecting Website -> ASP.NET Configuration and you’ll see the ASP.NET
Web Site Administration Tool in Figure 8.

Figure 8. Web Site Administration Tool Main Screen

Web Site Administration Tool Main Screen

Your first task is to configure a provider. This will specify the database for storing
security objects. Click the Provider Configuration link to set up the database,
shown in Figure 9.

Figure 9. Specifying a Provider for Security Services

Specifying a Provider for Security Services

While you have the option of selecting a different provider for each ASP.NET feature
(i.e. membership, roles, profiles, etc.), it is more common to select a single provider,
which is the approach this article takes. Click the Select a single provider for
all site management data
link, and observe the next screen in Figure 10,
showing the AspNetSqlProvider.

Figure 10. The Default ASP.NET Security Provider

The Default ASP.NET Security Provider

Next, click on the Security tab to set up an administrator, roles, and site
security settings, showing the screen in Figure 11.

 Figure 11. Configuring Security

Configuring Security

The easiest way to configure security is by using the provided wizard, so click
on Use the security Setup Wizard to configure security step by step. Click
Next to bypass the Welcome screen and you’ll see the Select Access Method
screen in Figure 12.

Figure 12. Selecting an Access Method

Selecting an Access Method

The access method defaults to From a local area network, which means windows
authentication. What you really want is ASP.NET Forms authentication, so select
From the internet. This assumes that the Web site will be deployed to the
internet and accessed from any platform, including non-Windows platforms. Therefore,
ASP.NET Forms authentication would be appropriate. Windows authentication would
be a better choice if the Web site was on a network inside of a company where users
could use their domain credentials to access the site.

After you select From the internet, click on Next to view the Advanced
provider settings screen. We’ve already discussed provider settings, so click on
Next again for the Roles screen, shown in Figure 13.

Figure 13. Enabling Roles

Enabling Roles

ASP.NET Roles are managed via cookies and checking the box on the Roles screen,
Figure 13. In most implementations, you will want roles. i.e. Admin, Staff, etc.
Click Next to create a new role, shown in Figure 14.

Figure 14. Adding a New Role

Adding a New Role

Something you’ll want to do when setting up security is to create a role for administrators.
Type in Admin, Figure 14, and click the Add Role button. Add any other
roles you might want for the site. Click the Next button to create a user,
shown in Figure 15.

Figure 15. Adding a New User

Adding a New User

In addition to an Administrator role, you’ll also need to create the user account
for an administrator. The UserName and Password are self-explanatory. The default
password configuration is a minimum of 7 characters and one 1 non-alphanumeric character.
You’ll receive an error if the password doesn’t meet the complexity requirements.
Later, I’ll show you how to configure passwords, via the web.config membership element,
to alter complexity requirements. By default, user names and emails must be unique.
The Security Question and Security Answer are used to challenge the user when they
are changing passwords. User status is Active by default, but you can uncheck the
Active User box to create the user without allowing them to log in. Click the Create
User
button to add the user. Click Next to configure site security,
shown in Figure 16.

Note: One of the things you should notice about creating the user, Figure
15, is the minimal amount of information required. This is inadequate because most
applications will have additional information about users, such as contact information.
Later, I’ll show you how to resolve this problem.

Figure 16. Configuring Site Security

Configuring Site Security

The key to understanding site security policy is “First match wins”. ASP.NET will
read configuration settings from the top down. If it finds a match, then it stops
looking and uses the configuration that matches the user configuration. You can
configure policy based on users or roles. In most cases, you will build policy based
on roles because it allows you to add and remove any number of users to and from
those roles without changing policy. We’ll take the role-based approach in this
article.

To configure Web site security policy, select the AspDotNetSecurity folder,
click on the Roles option and ensure that Admin is selected, click
on the Allow permission, and then click the Add This Rule button.
Next, you’re going to add another rule, by selecting the All Users option,
select the Deny permission, and then click the Add This Rule button.
You can see the two rules in the list below the input controls, showing Allow Admin
first and then Deny [all]. This gives access to anyone in the Admin
role to the entire site, but prevents all other users from accessing the site. You
can add more roles above the Deny All Users entry to permit other roles to
use the site.

Note: Remember the first match wins rule because if the Deny [all]
were at the top of the list, no one would be allowed to log in; this is a common
gottcha for new ASP.NET developers.

Going back to the folder list, where you clicked on the AspDotNetSecurity
folder, if you opened lower level folders in the site, you could also configure
security policy for any of those folders also. Security policy applies to a specified
folder and all other folders below it, unless the lower level folders have their
own security policy, which would override the security policy of the parent folder.
Click Next and then click Finish.

You have one more task to complete, associating the Admin user with the Admin
role. Click on the Security tab, select Manage Users, click Edit Roles
for the Admin user
, and check the Admin box. Close the ASP.NET Web
Site Administration Tool. Your site security is now set up and you can begin adding
controls.

Overview of Login Controls

ASP.NET ships with a set of controls for managing security on a Web site. You can
see what is available via the Toolbox, which you can open by selecting View ->
Toolbox and then expanding the Login tab. Since the Toolbox is context sensitive,
you’ll need to have an *.aspx page open in the designer to see its contents; you
can double-click on Default.aspx in the Solution Explorer to do this. As shown in
Figure 17, there are several Login controls available.

Figure 17: Login Controls

Login Controls

Table 1 describes the controls you see in Figure 17.

Table 1. ASP.NET Login Controls
Control Name Purpose
Login Allows user to log in with user name and password
LoginView Contains templates that can be configured to display based on logged in user’s roles
PasswordRecovery Allows users to recover passwords
LoginStatus Displays whether user is logged in or logged out
LoginName Displays logged in user’s name
CreateUserWizard Allows registration of a user in security system
ChangePassword Lets a user change their password

You’ll see each of these controls used in this article, plus coverage of workarounds
that are similar to the practical scenarios you implement every day.

Creating a Login Page

As it stands right now, no one can use your site. This is because security policy
is set up to deny all unknown users, except for those in the Admin role. However,
there isn’t a way for the site to know if someone is in the Admin role because logins
haven’t been set up yet. We’ll fix that now.

First, we’ll set up a page, representing site content, and then create a login page.
We’ll use Default.aspx as content that you might want to protect on a site. To let
you know you’re on the right page, type “This is the default content page.” into
the form area of Default.aspx.

Next, add a page named Login.aspx to your site. It is important that you use this
name for your page because it is the default name that ASP.NET uses whenever a user
must be authenticated. Although there are advanced options, which you can set via
web.config, naming your login page as Login.aspx is normal.

Drag-and-drop a Login control from the Toolbox to the Login.aspx page and click
on Design view, shown in Figure 18.

Figure 18. The Login Page

The Login Page

Right-click on Default.aspx and select Set as Startup Page. This will ensure
that when we run the application that it tries to open Default.aspx first. However,
it can’t do this because the site security policy is set to deny all unknown users,
which will cause ASP.NET to redirect to the login page. After we login, ASP.NET
will redirect to Default.aspx because it keeps track of what your original destination
was so you can go straight there after logging in. Run the application and log in
to observe this behavior.

If that doesn’t work, you might have missed a step in setting everything up, so
review each step to make sure you don’t miss anything. Hopefully, I’ve identified
enough hazards for you to avoid and make it this far successfully.

Changing Passwords

You might like implementing this; Create a new Web page named ChangePassword.aspx.
Drop a ChangePassword control onto the page. That’s it.

To try it out, run the application, log on, change the browser address from Default.aspx
to ChangePassword.aspx and fill in the form. Close and reopen the browser window
and log in with your new password.

Registering New Users

Now that I’ve given you a break by showing a couple easy tasks, let’s move back
into more complex topics by discussing proper registration and management of users.
If you recall, during security setup, creating a user offered a minimal set of user
information. In real applications, this is insufficient because you need to keep
track of other user data, such as address and other contact info. Therefore, you
need a workaround for both allowing new users to register and/or managing existing
users.

The ASP.NET Configuration Tool is insufficient for this task because it only updates
login information that ASP.NET needs. If you have other user information, it will
never be created or updated because the ASP.NET Configuration Tool has no knowledge
of this information. You can use the ASP.NET Configuration Tool for general site
configuration and roles, but you should not use it for user management because it
doesn’t keep all the user data in sync. I needed to create the initial user to log
in, but that isn’t ideal because now that user doesn’t have updated contact info.

Looking for a built-in solution, one might be tempted to look at ASP.NET Profiles,
which allows you to store personalization data for each user. The problem with profiles
is that the default database configuration is a proprietary format with special
configuration for tables. This makes it convenient for strongly typing the data
and accessing it via profile properties, but it doesn’t work well if you need to
query the table, which would be onerous because of the proprietary format. There
are other options to extend profile and use normal tables, but it seems like too
many gyrations to do something that should be simple. My preferred solution is to
create my own Users table. Add the Users table, shown below, to your database:

CREATE TABLE [dbo].[Users](
     [UserName] [nvarchar](256NOT NULL,
     [Address] [nvarchar](50NOT NULL,
     [City] [nvarchar](50NOT NULL,
     [State] [nvarchar](50NOT NULL,
     [PostalCode] [nvarchar](20NOT NULL,
     [Phone] [nvarchar](50NOT NULL,
  CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED 
 
 (
     [UserName] ASC
 )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, 
ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ONON [PRIMARY]
 ) ON [PRIMARY]

The benefit of this approach is that I can create a one-to-one relationship between
the ASP.NET aspnet_users table and my own table. This required a little bit
of knowledge of the aspnet_users table, even though I made an earlier remark
about not needing to know much about the ASP.NET tables; this is an exception to
the rule. The primary key, UserName, has a one-to-one relationship with the
UserName field in the aspnet_users table. You might want to change
the primary key to the UserId, which is a GUID, but that’s up to you. I’ll
use UserName in this article.

Additionally, you’ll notice that I didn’t add any constraints to the Users
table. This would cause problems with the ASP.NET infrastructure . Besides, the
data is already inconsistent because the Admin user resides in aspnet_users,
but not in Users. You can resolve this now by adding a record where the UserName
is set to Admin and then fill in the rest of the data as required. You’ll have to
add the record manually because we haven’t created an input form for it yet, but
that will be resolved soon.

The next problem to solve is how to use the existing CreateUserWizard control to
save this data to both the ASP.NET tables and the User table. Fortunately, the CreateUserWizard
control has the ability to add templates with extra information. To get started,
add a new page to the site named Register.aspx and drag-and-drop a CreateUserWizard
control onto Register.aspx. Rename the CreateUserWizard control to RegisterWiz.
Go to Design view, click on the Action list, and click Add/Remove Wizard Steps.
You’ll see the Wizard Step Collection Editor, shown in Figure 19.

Figure 19. Adding a Step to the CreateUserWizard

Adding a Step to the CreateUserWizard

You should change the title of the step to User Info, use the up and down
arrows to place it as the first item in the list (Figure 19), and click OK when
you’re done. Next, select User Info from the CreateUserWizard control’s Action
List. Populate the User Info template with fields for the Users table, as shown
in Figure 20.

Figure 20. The User Info Template

The User Info Template

Just like any other user control with templates, you can drag-and-drop controls
into the template on the design surface. In this case, I used Label and TextBox
controls in a table. For the address, city, state, postal code, and phone number;
rename the TextBox controls to txtAddress, txtCity, txtState,
txtPostalCode, and txtPhone; respectively. Now that you know how to
add controls to collect custom data, the next section will explain how to properly
save this custom data to the database.

Saving Custom User Data

In most data entry scenarios, you simply grab the data and use your data access
technology of choice to save the data in the database. However, it isn’t as simple
in this case because you have ASP.NET user data with a one-to-one relationship with
the Users table. For data consistency purposes, you’ll need to ensure that both
of these tables are updated in the same operation.

Your first thought might be to perform the update via a transaction, but that isn’t
possible because the CreateUserWizard control saves the ASP.NET side of the data
automatically and you don’t have a hook into that part of the process. i.e. there
is not an event you can subscribe to or a virtual method you can override to perform
this operation yourself. While you can’t perform an atomic operation, a compensation
strategy would work.

Examining the CreateUserWizard, there are three events associated with user creation:
CreatingUser, CreateUserError, and CreatedUser. CreatingUser
occurs before the CreateUserWizard saves the ASP.NET data. CreateUserError
occurs if the CreateUserWizard encounters an exception while trying to save. CreatedUser
occurs after the CreateUserWizard has saved the ASP.NET data. If an error occurs
and the CreateUserWizard invokes CreateUserError, the CreateUserWizard will
not invoke CreatedUser.

Based upon the events of the CreateUserWizard and their behaviors, you can design
a compensation strategy to ensure the consistency of data in both the ASP.NET and
custom user data. Here, we have two conditions to address: ASP.NET update failure
or custom Users data update failure. If one update fails, the other should not persist.
The ASP.NET update failure case can be handled by the behavior of the CreateUserWizard
events. Since errors during update will invoke the CreateUserError event,
but not the CreatedUser event, we can put the Users table update code in
the CreatedUser event handler and know that it won’t be executed if an error
occurs with the ASP.NET update. In the case of an error with the Users table update,
the ASP.NET update has already occurred. Therefore, whenever an error occurs during
the Users table update, we must delete the user from the ASP.NET tables to maintain
consistency. Listing 1 shows an implementation of the CreatedUser event handler
that saves user data and compensates for potential errors during the update.

Listing 1. Implementing the CreatedUser Event for the CreateUserWizard
    protected void CreateUserWizard1_CreatedUser(object sender, EventArgs e)
    {
        var txtAddress = RegisterWiz.FindControl("txtAddress") as TextBox;
        var txtCity = RegisterWiz.FindControl("txtCity") as TextBox;
        var txtState = RegisterWiz.FindControl("txtState") as TextBox;
        var txtPostalCode = RegisterWiz.FindControl("txtPostalCode") as TextBox;
        var txtPhone = RegisterWiz.FindControl("txtPhone") as TextBox;

        var user = new UserInfo
        {
            UserName = RegisterWiz.UserName,
            Address = txtAddress.Text,
            City = txtCity.Text,
            State = txtState.Text,
            PostalCode = txtPostalCode.Text,
            Phone = txtPhone.Text
        };

        var userMgr = new UserManager();

        try
        {
            userMgr.InsertUser(user);
            Roles.AddUserToRole(RegisterWiz.UserName, "Admin");
        }
        catch (Exception)
        {
            // compensating action to ensure data
                consistency
            Membership.DeleteUser(RegisterWiz.UserName);

            throw; 
            // and/or log 
            // and/or communicate with user

            // and/or some responsible handling
                technique
        }
    }

There are a couple tasks being performed in Listing 1 to prepare the update, prior
to error handling: getting a reference to custom data controls, instantiating an
entity object with the proper info, and referencing a business object to interact
with. The following snippet repeats a line from Listing 1 that demonstrates how
to access custom controls in the CreateUserWizard, RegisterWiz:

        var txtAddress = RegisterWiz.FindControl(
    "txtAddress") as TextBox;

Like many other container controls in ASP.NET, the CreateUserWizard has a FindControl
method that will return a reference to a child control with a specified name. In
this case, the ID of the child control in the HTML is txtAddress, which is
passed as a string parameter to FindControl. The return value of FindControl
is Control, so we need to perform the conversion to TextBox, using
the as operator. This results in a reference to the txtAddress control
inside the User Info template of RegisterWiz. This example uses C# 3.0, and
later, object initialization syntax to create an instance of UserInfo, a
custom type passed into the business logic layer, repeated below:

        var user = new UserInfo
        {
            UserName = RegisterWiz.UserName,
            Address = txtAddress.Text,
            City = txtCity.Text,
            State = txtState.Text,
            PostalCode = txtPostalCode.Text,
            Phone = txtPhone.Text
        };

It uses the UserName from the CreateUserWizard, which is important because
UserName is the key of the Users table. All of the other native control values
of CreateUserWizard are directly accessible via RegisterWiz instance properties.
It’s only the custom controls that you add to a template where a call to FindControl
is required. Looking at the downloadable code associated with this article, you’ll
notice that I used the ADO.NET Entity Framework and LINQ to Entities for working
with the Users table. Implementing this with LINQ was my preference, but you can
use any data access technology that you’re comfortable with. The particular implementation
is out of scope for this article, but hopefully the fact that I abstracted the implementation
by placing it in a business object will make this part of the code more understandable.
The code below, taken from Listing 1, demonstrates how I instantiate the business
object that adds the new user info to the database:

        var userMgr = new UserManager();

The UserManager is a custom business object that I added to a library project
and referenced from the current Website project. It exposes an InsertUser
method that will perform the logic necessary to validate and save this object. The
following snippet from Listing 1 shows how to call InsertUser and properly
handle errors:

        try
        {
            userMgr.InsertUser(user);
            Roles.AddUserToRole(RegisterWiz.UserName, "Admin");
        }
        catch (Exception)
        {
            // compensating action to ensure data
                consistency
            Membership.DeleteUser(RegisterWiz.UserName);

            throw; 
            // and/or log 
            // and/or communicate with user

            // and/or some responsible handling
                technique
        }

It’s important that any exceptions raised from calling InsertUser are handled
properly. Your own business situation and requirements dictate what the complete
handling strategy should be, which is why I added all of the annoying comments reminding
you to implement a proper handling strategy. Part of the handling strategy must
be to delete the user from the ASP.NET tables, which is accomplished via the call
to Membership.DeleteUser. Remember, by the time that the CreatedUser
handler executes, ASP.NET has already saved the user in its tables. An exception
during the call to InsertUser means that the new data has not been saved
to the Users table, which would leave the database in an inconsistent state. Therefore,
deleting the ASP.NET user data restores consistency.

Note: The discussion on exception handling strategy makes the assumption
that Users data was not saved. Of course, there are exceptions to the rule. What
If you were doing something in business logic that caused the exception after the
value was saved? Again, the code you’ve written and application requirements dictate
the exact exception handling strategy.

Notice that the code also assigns the user to the Admin role. If you recall, when
setting up security policy for this site, we allowed only users in the Admin role
to access pages. In practice, you would assign users to some default role and set
security policy to allow those users to access only parts of the Web site that you
allowed them to see. Additionally, I hard-coded Admin, but you might need this to
be configurable via database or appSettings in web.config. Now, you can run
the application.

Navigate to Register.aspx after logging in and fill in the form. If there aren’t
any errors, you’ll be able to open the aspnet_users and Users tables
and verify that the UserName you entered resides in both tables. To test
the ASP.NET update failure scenario, you can try registering a new user with a duplicate
UserName and verifying that the record was not added to either table. To
test the Users table failure scenario, you can alter the InsertUser code
and throw an exception before saving the data and then verify that the user is not
part of either table.

Now you have a valid user in your system with custom data that is consistent with
ASP.NET data. This gives you the best of both worlds where you can have additional
user data, beyond what ASP.NET provides, and still retain the benefits of using
the built-in ASP.NET security system.

Warning: With registration set up to write consistently to both ASP.NET tables
and the Users table, you don’t want to use the ASP.NET Web Administration tool to
add users. The ASP.NET Web Administration Tool has no knowledge of your Users table
and would cause your data to be inconsistent if you used the ASP.NET Web Administration
Tool to add users. A common symptom of this problem is when a user can log in, because
ASP.NET is aware of the user, but application functionality isn’t working, because
you don’t have custom data for the user.

Another site security feature is to help people who have an account, but have forgotten
their password, discussed next.

Implementing Password Recovery

With so many sites with their own user IDs and passwords, a common occurrence is
to forget a password. Just as common is the ability of sites to allow you to either
retrieve or reset your password. This section will explain how you can implement
password recovery in ASP.NET.

To get started, create a new Web page named ForgotPassword.aspx and drag-and-drop
a PasswordRecovery control onto it. Next, make sure you’re in Design view and then
click on the Administer Website link on the PasswordRecovery control’s action
list. You’ll see the ASP.NET Web Administration Tool appear, just like Figure 8.

The PasswordRecovery control will send an email message to the email address for
the user. To enable this behavior, you’ll need to configure an email server to relay
the email through. You can do this by clicking on the Application tab and
then click on the Configure SMTP e-mail settings link. At this point I would
show you a screen shot, but I’m sure you don’t want all of the details for logging
into my mail server. After you’ve added your own mail server credentials, click
the Save button, click the OK button, and then you can close the ASP.NET
Web Administration Tool screen.

Password Recovery is now set up, but you can’t use it yet. That’s because site security
policy won’t allow anonymous users to access any page other than Login.aspx. Since
the user doesn’t know their password, there is no way to get to the ForgotPassword.aspx
page, but there is a way to configure exceptions like this. Add a location element
to web.config, as shown in Listing 2.

Listing 2. Authorizing Access to Web Pages with the location Element
<?xml version="1.0"?>
 <configuration>
 ...
 <location path="ForgotPassword.aspx">
 <system.web>
 <authorization>
 <allow users="*"/>
 </authorization>
 </system.web>
 </location>
 </configuration>

Notice that the location element is a direct child of the configuration
element. The path attribute specifies the page and the users attribute
of the allow element specifies who can access the page, which is everyone,
as indicated by the star.

Tip: Using the location element is a useful technique for allowing access
to other parts of a Web site such as themes, resources, and other pages that you
want the public to have access to. If you aren’t able to access a Web Service, adding
a location element can help too. One indicator of the need for a location tag is
if your themes or styles don’t appear on Login.aspx or another page that you’ve
given the general public access to, which indicates a need to include your theme
and/or styles folder in a location tag. Just use a folder name and all the contents
below that folder will have the specified access.

Now, you can test password recovery by navigating from the login page to ForgotPassword.aspx.
Enter a user name, answer the security question, and then look for the message in
your email box. When setting up the user, you should have used an email address
that was real and that you have access too. Otherwise, someone else might get spammed.
Of course, it’s just as fine to use your boss’ email address too.

Modifying User Data

If you recall from earlier discussions, you should not use the ASP.NET Web Administration
Tool for updating user information. It doesn’t have any features that allow you
to update your custom user tables. Therefore, you must create your own Web page
for performing inserts, updates, deletes, and viewing the user list. This Web page
will be designed to use a custom object that ensures the consistency of user data.

A Bindable Business Object for Managing User Data

The technique you’ll learn in this article for managing the consistency of user
data uses transactions. Both the update to the ASP.NET tables and the custom Users
table will occur in the scope of a single transaction. That way, if an error occurs
during either update, all changes roll back, leaving the database in the consistent
state it was in before the transaction started. Listing 3 contains a business object
that shows how to properly manage user data. To run the code, you need to add a
reference to the System.Transactions assembly and add a using declaration to the
file for the System.Transactions namespace.

Listing 3. A Business Object that Manages User Data via Transactions
using System;
using System.Collections.Generic;
using System.Linq;
using System.Transactions;
using System.Web.Security;
using System.ComponentModel;

namespace SecurityLib
{
    /// <summary>
    /// manages users
    /// </summary>
    [DataObject]
    public class UserManager
    {
        /// <summary>
        /// reference to DAL
        /// </summary>
        private UserData m_data = new UserData();

        /// <summary>
        /// ensures a valid UserInfo object
        /// </summary>
        /// <param name="user">UserInfo to check</param>
        private void ValidateUserInfo(UserInfo user)
        {
            if (user == null)
            {
                throw new ArgumentNullException(
                    "user", 
                    "user must refer to a valid Users instance");
            }

            if (string.IsNullOrEmpty(user.UserName))
            {
                throw new ArgumentException(
                    "user must contain a UserName property with a valid value.", 
                    "user.UserName");
            }
        }

        /// <summary>
        /// returns a list of all users from
        /// both ASP.NET and Users tables
        /// </summary>
        /// <returns>list of UserInfo</returns>
        [DataObjectMethod(DataObjectMethodType.Select)]
        public List<UserInfo> GetUsers()
        {
            var allUsers =
                from user in m_data.GetUsers()
                join MembershipUser mbrUser in Membership.GetAllUsers()
                    on user.UserName equals mbrUser.UserName
                select new UserInfo
                {
                    UserName = mbrUser.UserName,
                    Password = string.Empty,
                    Email = mbrUser.Email,
                    Address = user.Address,
                    City = user.City,
                    State = user.State,
                    PostalCode = user.PostalCode,
                    Phone = user.Phone
                };

            return allUsers.ToList();
        }

        /// <summary>
        /// adds a new user to both
        /// ASP.NET and Users tables
        /// </summary>
        /// <param name="user">UserInfo to add</param>
        [DataObjectMethod(DataObjectMethodType.Insert)]
        public void InsertUser(UserInfo user)
        {
            ValidateUserInfo(user);

            using (var tx = new TransactionScope(
                                TransactionScopeOption.Required))
            {
                var userEntity = new Users
                {
                    UserName = user.UserName,
                    Address = user.Address,
                    City = user.City,
                    State = user.State,
                    PostalCode = user.PostalCode,
                    Phone = user.Phone
                };

                // add to the Users table
                m_data.InsertUser(userEntity);

                // add to ASP.NET tables
                Membership.CreateUser(user.UserName, user.Password, user.Email);

                // no exception - commit
                tx.Complete();
            }
        }

        /// <summary>
        /// updates specified user
        /// </summary>
        /// <param name="user">user to update</param>
        [DataObjectMethod(DataObjectMethodType.Update)]
        public void UpdateUser(UserInfo user)
        {
            ValidateUserInfo(user);

            using (var tx = new TransactionScope(
                                TransactionScopeOption.Required))
            {
                var userEntity = new Users
                {
                    UserName = user.UserName,
                    Address = user.Address,
                    City = user.City,
                    State = user.State,
                    PostalCode = user.PostalCode,
                    Phone = user.Phone
                };

                // modify the Users table
                m_data.UpdateUser(userEntity);

                var mbrUser = Membership.GetUser(user.UserName);
                mbrUser.Email = user.Email;

                // modify ASP.NET tables
                Membership.UpdateUser(mbrUser);

                // no exception - commit
                tx.Complete();
            }
        }

        /// <summary>
        /// deletes specified user
        /// </summary>
        /// <param name="user">contains primary key, UserName</param>
        [DataObjectMethod(DataObjectMethodType.Delete)]
        public void DeleteUser(UserInfo user)
        {
            ValidateUserInfo(user);

            using (var tx = new TransactionScope(
                                TransactionScopeOption.Required))
            {
                var userEntity = new Users
                {
                    UserName = user.UserName
                };

                // delete from the Users table
                m_data.DeleteUser(userEntity);

                // delete from ASP.NET tables
                Membership.DeleteUser(user.UserName);

                // no exception - commit
                tx.Complete();
            }
        }
    }
}

The UserManager object in Listing 3 has validation and methods for working
with UserInfo objects. It also contains an m_data field for referencing
the Data Access Layer (DAL). As stated earlier, I used LINQ to entities to implement
the DAL, but you can use any data access technology that will enlist in a transaction.

Notice how InsertUser, UpdateUser, and DeleteUser in Listing
3 have using statements for TransactionScope objects. The using
statement defines the boundaries of the transaction. The code sets TransactionScopeOption
to Required, guaranteeing that the transaction runs either as part of another
transaction or starts a new transaction. The important part of these methods is
that they operate on both the ASP.NET user info and the custom user info. The code
uses the ASP.NET membership API to update ASP.NET data and the DAL to update custom
data at the same time. Both updates must occur atomically or not at all, which is
where the transaction logic helps out.

If the transaction runs successfully, the tx.Complete statement will execute,
committing the transaction. If an exception occurs, there is no way that the tx.Complete
will be called, meaning that the transaction will not be committed. As you may already
know, parameters to a using statement must be IDisposable, meaning
that the using statement will call Dispose on the TransactionScope
instance, tx. The TransactionScope instance knows whether Complete
has been called. If Dispose executes and the transaction has not been committed,
then Dispose will ensure that the transaction rolls back, leaving the database
in the consistent state it was in before the transaction.

Note: Remember to make the call to Complete on the TransactionScope
instance as the last statement of the using statement. You don’t want to
accidentally commit and then have some code subsequently throw an exception and
leave your database in an inconsistent state.

Note: When implementing transactions with TransactionScope, you might need
to turn on the Distributed Transaction Coordinator (DTC).  As soon as you receive
an exception that tells you to enable DTC, you’ll know you need to do this.
Here’s a blog entry to point you in the right direction: Enabling DTC for TransactionScope in Vista.

Now that the code is in place for working with transactions, let’s use it.

Building a User User Interface

You aren’t seeing double or a typo, we’ll be building a user interface to manage
users in this section. You’ll see how to construct a ListView control that works
with the UserManager business object shown in Listing 3. This will allow
viewing, adding, modifying, and deleting user data. You won’t care whether it comes
from ASP.NET or the Users table because the UserManager abstracts those details
from you.

To get started, create a new Web page named ManageUsers.aspx and drag-and-drop a
ListView control onto it. In the action list for the new ListView control, select
New data source from the Choose Data Source drop-down list, which
will show the Choose a Data Source Type screen in Figure 21.

Figure 21. Choosing a Data Source Type

Choosing a Data Source Type

On the Choose a Data Source Type window, select Object, type UserDataSource
as the ID, and click the OK button. You’ll see the Choose a Business
Object
screen, shown in Figure 22.

Figure 22. Choosing a Business Object Type

Choosing a Business Object Type

Select the UserManager object from the drop-down list in the Choosing a Business
Object Type
window and click Next to show the Define Data Methods
window in Figure 23.

Figure 23. Defining Data Methods

Defining Data Methods

There is a tab on the Defining Data Methods window for each operation you
need to perform: select, insert, update, and delete. You should choose each tab
and select the proper UserManager method for that operation. Then click the Finish
button. You’ll see that the ObjectDataSource control has been added to the page,
but the ListView control is still blank, which we’ll work on next.

The action list for the ListView control now has a new link, named Configure ListView.
Click the Configure ListView link, which will show the Configure Listview
window in Figure 24.

Figure 24. Configuring a ListView Control

Configuring a ListView Control

In the Configure ListView window, ensure that Grid is selected and
that all of the check boxes are checked, exactly as shown in Figure 24. Then click
the OK button.

For delete functionality to work, you must set the primary key in the ListView.
To implement this, click on the ListView control in Design view, open the Properties
window, and add “UserName” to the DataKeyNames property. If the
ListView control doesn’t have a key, the validation logic in the UserManager business
object will throw an exception because the ObjectDataSource passes a UserInfo with
a null UserName.

You now have a designer page with the ListView showing all columns as defined by
the UserInfo object. VS 2008 can infer the object type from the return type
of the select method. As you might recall, the select method specified when defining
data methods for the ObjectDataSource is the GetUsers method, which returns
a List<UserInfo>.

You have the option to modify the ListView template as you need, which is beyond
the scope of this article. For example, you might not want the Password column
to appear in any of the templates, except for the InsertItemTemplate. Another
modification might be to change the UserName column in the EditItemTemplate
to a Label control to keep from modifying the value; seeing as it is the record
key and maintains the one-to-one relationship between the aspnet_users and
Users table.

Test the functionality of the ListView by running the site and navigating to ManageUsers.aspx.
Most of the work here was setting up business objects properly, making sure that
transactions occurred properly.

This is almost a workable user management console that allows you to maintain custom
user data in a consistent manner. The last problem to overcome is that you can’t
insert a new user record because it expects the security question and answer, which
isn’t supplied in the business object logic. You can either add the security question
and answer to UserInfo and implement it in the InsertUser method or
configure your application to not require the functionality of security question
and answer. The next section shows you how to turn off security question and answer
in addition to other options that enable you to modify the ASP.NET membership configuration.

Customizing Membership Configuration

ASP.NET security defaults tend to be set in the direction of better security. This
is good and follows the Microsoft philosophy of defaulting to a more secure mode.
However, you’ll often find that customers prefer less security, which improves their
convenience. The practicalities of being on the internet eventually leads to a solution
that is somewhere in-between what the customer asks for and what the customer really
should have in the way of security. Therefore, you might be pleased to learn that
you do have the ability to customize security settings, tweaking them just-right
for your application.

This section will show you how to customize membership settings. I won’t show you
everything, but will concentrate primarily on the areas where you are most likely
to want to customize, such as security question and answer and password complexity.

Customizing membership can be implemented via a membership element in web.config.
Listing 4 contains an example that you can modify and use in your own applications.

Listing 4. Configuring Membership via Web.config
<configuration>
 ...
 <system.web>
 ...
 <membership defaultProvider="AspNetSqlProvider">
 <providers>
 <clear/>
 <add name="AspNetSqlProvider"
 type="System.Web.Security.SqlMembershipProvider,
 System.Web,Version=2.0.0.0,
 Culture=neutral,
 PublicKeyToken=b03f5f7f11d50a3a"
 connectionStringName="LocalSqlServer"
 applicationName="/"
 enablePasswordReset="true"
 requiresQuestionAndAnswer="false"
 minRequiredPasswordLength="6"
 minRequiredNonalphanumericCharacters="0"
 maxInvalidPasswordAttempts="100" />
 </providers>
 </membership>
 ...
 </system.web>
 ...
 
 </configuration>

There are a few important attributes to point out in the membership element’s
provider definition. First, connectionStringName refers to the name
of the connection string we added earlier in this article, LocalSqlServer,
which refers to the database where the ASP.NET tables for this application are loaded.

Some customers don’t like ASP.NET’s security question and answer feature. You can
turn this off by setting the requiresQuestionAndAnswer attribute to false.
This means that the CreateUserWizard and PasswordRecovery control won’t ask you
for the security question and answer anymore. Also, the call to CreateUser
on during the InsertUser method doesn’t require the overload that takes question
and answer parameters.

Another common customer request is to reduce password complexity, especially the
non-alphanumeric character requirement. The minRequiredPasswordLength attribute,
as its name suggests, reduces the number of characters required in the password.
Setting the minRequiredNonalphanumericCharacters attribute to 0 will eliminate
the requirement to have any non-alphanumeric characters. Of course there are some
customers who want stronger security and you can tweak the settings the other way
to make the password requirements more stringent. If password requirements differ
from what can be set via minRequiredNonalphanumericCharacters or minRequiredPasswordLength,
you should look at setting the passwordStrengthRegularExpression, not shown
in Listing 4.

There are many more options to set; more so than can be explained in this article.
However, these were some of the membership configuration options that are more likely
to motivate customer inquiry.

Another thing that customers notice is when you personalize the site a bit for them.
The next section will show you how to greet a customer and manage the Login and
Logoff process a little more conveniently.

Handling Login Status

A nice touch of personalization to add to a site is to remember who a customer is
and greet them after logging in. Another usability feature for sites is to allow
users to explicitly log in or out. This section will show you how to accomplish
these tasks with the LoginView, LoginStatus, and LoginName controls.

The LoginView control helps manage content, based on the user’s current role or
login status. It offers a template for either anonymous or logged in users. You
can also add templates for displaying information to users in specific roles. This
article will show how to manage display information based on whether a user is logged
in or not.

A common place to add login status information is on a master page, which is viewable
by all content pages. For simplicity, this article only uses Default.aspx, but the
same concepts apply regardless of where you place the login controls.

To get started, add a LoginView control to Default.aspx, preferably at the top of
the page where you would normally find login status information. If you open the
action list for the LoginView control, you’ll see a Views drop-down list
with entries for AnonymousTemplate and LoggedInTemplate.

Select the LoggedInTemplate and drag-and-drop a LoginStatus control into
the template, and set the LoginStatus to LoggedIn. Now, run the application,
log in, and observe that the LoginStatus control say’s Logout, assuming you
landed on Default.aspx. Click on the Logout link and observe that you’ve
been logged out and redirected to Login.aspx.

After you shut down the application and navigate back to the LoginView control on
Default.aspx, ensure you are viewing the LoggedInTemplate that contains the
LoginStatus control. Now, type “, Hi “ after the LoginStatus control and then drag-and-drop
a LoginName control after the greeting. Run the application again, log in, and verify
that you see the greeting with your UserName on Default.aspx.

Tip: You can extend LoginView to show only selected content on the same page,
depending on the role of the user. Just click on the Edit RoleGroups link
on the LoginView action list and add new templates in the editor. Remember the first
match wins rule, meaning that if a user is in multiple roles, the first template
in the list with a role will determine what the user sees. After you add the template,
you can return to the LoginView, select that template in the Views drop-down
list and add the content you want. A gottcha on using the LoginView this way is
that the more you customize a single page, the more complex UI maintenance will
become. Therefore, implement new LoginView roles in moderation and it can be very
useful.

Summary

This has been a whirlwind tour of implementing ASP.NET security on a Web site. You
learned how to set up a database with ASP.NET tables and how to build a Users table
with a one-to-one relationship with the ASP.NET aspnet_users table. This set the
stage for much of the additional content in the article, showing you a strategy
for managing custom user data, while enjoying the benefits of ASP.NET security.
To manage this custom user data and keep data consistent, you saw how to implement
compensation strategies with the CreateUserWizard and code transactions for managing
user data. You saw how easy it was to manage a login, change passwords, and help
users recover lost passwords. Additionally, you learned how to configure membership
so that you can customize the behavior of ASP.NET security, including the ability
to turn off security question and answers and modify password complexity requirements.
Finally, you saw how to add convenience to a site by allowing the user to log off
and increase personalization by greeting users who have logged on. There is many
more features to be covered in ASP.NET security but hopefully, you’ve learned how
to deal with several common scenarios and manage user information in a practical
manner.

Follow Joe Mayo on Twitter.

Feedback