Sunday 7 July 2013

Making Sense: ASP.NET Security

ASP.NET Security

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 asAspDotNetSecurity. 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 toLocalSqlServer 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 Rolebutton. 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 NamePurpose
LoginAllows user to log in with user name and password
LoginViewContains templates that can be configured to display based on logged in user’s roles
PasswordRecoveryAllows users to recover passwords
LoginStatusDisplays whether user is logged in or logged out
LoginNameDisplays logged in user’s name
CreateUserWizardAllows registration of a user in security system
ChangePasswordLets 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 theaspnet_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,txtCitytxtStatetxtPostalCode, 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: CreatingUserCreateUserError, 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 theCreatedUser 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 anInsertUser method that will perform the logic necessary to validate and save this object. The following snippet from Listing 1 shows how to callInsertUser 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 toMembership.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 Userstables 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 settingslink. 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 InsertUserUpdateUser, 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 usingstatement must be IDisposable, meaning that the using statement will call Dispose on the TransactionScope instance, tx. The TransactionScopeinstance 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 theConfigure 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 theUserName 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 requiresQuestionAndAnswerattribute 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. TheminRequiredPasswordLength attribute, as its name suggests, reduces the number of characters required in the password. Setting theminRequiredNonalphanumericCharacters 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 andLoggedInTemplate.
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 LoggedInTemplatethat 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.

No comments:

Post a Comment