Making Sense: ASP.NET Security
by Joe Mayo
created 1/8/09
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

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

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 from http://www.microsoft.com/express/sql/. 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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](256) NOT NULL,
[Address] [nvarchar](50) NOT NULL,
[City] [nvarchar](50) NOT NULL,
[State] [nvarchar](50) NOT NULL,
[PostalCode] [nvarchar](20) NOT NULL,
[Phone] [nvarchar](50) NOT 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 = ON) ON [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

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

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

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

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

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

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.
Your feedback and constructive contributions are welcome. Please feel free
to contact me for feedback or comments you may have about this lesson.
Copyright © 2000-2010 C# Station, All Rights Reserved