JavaScript EditorFreeware javascript editor     Javascript code


Main Page

Previous Page
Next Page

Design

To recap, the "virtual client" has commissioned a membership system that handles the following operations and features:

ASP.NET 2.0 introduces some great new features that help to develop the membership subsystem.

Password Storage Mechanisms

There are basically three methods for storing passwords, with each one offering different trade-offs between security and the convenience of developers, administrators, and users.

  1. The most convenient method of password storage for developers and administrators is to store the password as plain text in a database field. This is also convenient for users because you can easily e-mail a user's password to them in case they forget it. However, this is the least secure option because all of the passwords are stored as plain text — if your database were compromised by a hacker, he'd have easy access to everyone's password. You need to be extremely careful about locking down your database and ensuring that you secure your database backup files.

  2. To enhance the security of password storage you can encrypt the passwords before storing them in a database. There are many ways to encrypt passwords but the most common type is symmetric encryption, which uses a guarded system password to encrypt all user passwords. This is two-way encryption: You can encrypt a password and also decrypt it later. This offers medium convenience for developers, but still offers a lot of convenience for users because you can still e-mail them a forgotten password.

  3. The highest level of security requires a form of encryption that prevents administrators and developers from gaining access to any user's password. This uses a one-way type of encryption known as hashing. You can always encrypt a password by hashing the password with a proven algorithm but you can never decrypt it. Therefore, you store the hashed version of the password, and later, when you want to verify a user's password when he logs in again, you can perform the same hashing algorithm on whatever he types in as his password. You can then compare this hash against the hash you stored in the database — if the two match then you know the user entered his password correctly. This offers a low amount of convenience to developers, administrators, and users because it's not possible to e-mail forgotten passwords. Instead, if a user forgets his password, your only choice is to change the user's password to a known value, and then save the hash for his new password.

Hashing (method 3) was used in the first edition of this book, but it caused a lot of confusion for administrators and frustration for users because people generally prefer having the option of "recovering" a lost password without requiring a new one. We will use symmetric encryption (method 2) in this edition, but please keep in mind that password hashes should always be used to protect web sites containing financial data or other very sensitive data (such as medical records, test scores, etc.). Most users would not like to see their super-secret banking password mailed to them in an e-mail message, and most don't even want bank employees to have access to passwords. A bank employee who is trusted today might become a disgruntled former employee tomorrow, and it's nice to know that he won't be taking your password with him!

Authentication Modes: Windows Security or Custom Login Form?

The first thing you have to decide when you set up a security mechanism for a web site is whether you want to use Windows or forms authentication. Windows authentication is the easiest to set up and use, while forms authentication requires you to create a custom database and a login form. Windows security is usually the best choice when you are developing an intranet site for which all users who have access to the site are also users of a company's internal network (where they have domain user accounts). With Windows security, users enjoy the capability to use restricted web pages without having to formally log in to the web site, the page is executed under the context of the user requesting it, and security restrictions are automatically enforced on all resources that the code tries to access and use (typically files and database objects). Another advantage is that Windows will securely store and encrypt user credentials so you don't have to. However, the requirement to have a local network account is a huge disadvantage that makes it a bad choice for Internet sites. If you use Windows security for users located outside of a company's network, the company would be required to create a network user account for each web site user, which makes it slow for users to gain access and expensive for companies to administer. While you could conceivably write some code to automate the creation of Windows network accounts, and could write a login page that uses Windows impersonation behind the scenes, it just doesn't make sense to employ Windows security with those nasty workarounds in our context (a public web site with possibly thousands of users). Instead, it makes more sense for us to use forms authentication, and store user account credentials and related profile data in a custom database.

Important 

great new feature introduced in ASP.NET 2.0 is support for cookieless clients in forms authentication. ASP.NET 1.x supported the cookieless mode only for Session variables, but not for authentication, and you had to use dirty workarounds to make it work for cookieless authentication. Now, however, you only have to set the <forms> element's cookieless attribute in web.config to true or to AutoDetect. With AutoDetect, ASP.NET checks whether the user's browser supports cookies, and if so, uses a cookie to store a Session ID. If a user's browser doesn't support cookies, then ASP.NET will pass the Session ID in the URL. Only users who do not support cookies have to see a long and ugly Session ID in the URL.

The "Let's Do Everything on Our Own" Approach

Designing a module for handling user membership and profiling is not easy. It may not seem particularly difficult at first: You can easily devise some database tables for storing the required data (roles, account credentials and details, the associations between roles and accounts, and account profiles) and an API that allows the developer to request, create, and modify this data. However, things are rarely as easy as they appear at first! You must not downplay the significance of these modules because they are very crucial to the operation of the web site, and properly designing these modules is important because all other site modules rely on them. If you design and implement the news module poorly, you can go back and fix it without affecting all the other site's modules (forum, e-commerce, newsletter, polls, etc.). However, if you decide to change the design of the membership module after you have developed other modules that use it, chances are good that you will need to modify something in those modules as well. The membership module must be complete but also simple to use, and developers should be able to use its classes and methods when they design administration pages. They should also be able to create and edit user accounts by writing just a few lines of code or, better yet, no code at all. ASP.NET 1.1 provided a partial security framework that allowed you to specify roles that could or could not access specific pages or folders by specifying role restrictions in web.config. It also took care of creating an encrypted authentication cookie for the user, once the user logged in. However, the developer was completely responsible for all the work of writing the login and registration pages, authenticating the user against a database of credentials, assigning the proper roles, and administering accounts. In the first edition of this book we did everything ourselves, with custom code. The solution worked fine, but still suffered from a couple of problems:

  1. The developer had to perform all security checks programmatically, typically in the Page_Load event, before doing anything else. If you later wanted to add roles or users to the ACL (access control list) of a page or site area, you had to edit the code, recompile, and re-deploy the assembly.

  2. The membership system also included user profiling. The database table had columns for the user's first and last name, address, birth date and other related data. However, the table schema was fixed, so if you wanted to add more information to the profile later, you had to change the database, the related stored procedures, and many API methods, in addition to the user interface to insert the data.

Things could have been made more flexible, but it would have been more difficult to develop. You have to weigh the advantages of design extensibility against the time and effort to implement it. Fortunately, ASP.NET 2.0 has full-featured membership and profiling systems out of the box! Yes, that's right, you don't have to write a single line of code to register users, protect administrative pages, and associate a profile to the users, unless you want to customize the way they work (for example, to change the format in which the data is stored, or the storage medium itself).

This section first introduces the built-in security and profiling framework of ASP.NET 2.0; after that, you will learn how to profitably use it in your own project instead of "rolling your own" solution.

The Membership and MembershipUser Classes

The principal class of the ASP.NET 2.0's security framework is System.Web.Security.Membership, which exposes a number of static methods to create, delete, update, and retrieve registered users. The following table describes the most important methods.

Method

Description

CreateUser

Creates a new user account

DeleteUser

Deletes the specified user

FindUsersByEmail

Returns an array of users with the specified e-mail address. If SQL Server is used to store accounts, the input e-mail can contain any wildcard characters supported by SQL Server in LIKE clauses, such as % for any string of zero or more characters, or _ for a single character.

FindUsersByName

Returns an array of users with the specified name. Wildcard characters are supported.

GeneratePassword

Generates a new password with the specified length, and the specified number of non-alphanumeric characters

GetAllUsers

Returns an array with all the registered users

GetNumberOfUsersOnline

Returns an integer value indicating how many registered users are currently online

GetUser

Retrieves a specific user by name

GetUserNameByEmail

Returns the username of a user with the given e-mail address

UpdateUser

Updates a user

ValidateUser

Returns a Boolean value indicating whether the input credentials correspond to a registered user

Some of these methods (CreateUser, GetAllUsers, GetUser, FindUsersByName, FindUsersByEmail and UpdateUser) accept or return instances of the System.Web.Security.MembershipUser class, which represents a single user, and provides quite a lot of details about it. The following table describes the instance properties and methods exposed by this class.

Property

Description

Comment

A comment (typically entered by the administrator) associated with a given user

CreationDate

The date when the user registered

Email

The user's e-mail address

IsApproved

Indicates whether the account is enabled, and whether the user can log in

IsLockedOut

Indicates whether the user account was disabled after a number of invalid logins. This property is read-only, and the administrator can only indirectly set it back to false, by calling the UnlockUser method described below.

IsOnline

Indicates whether the user is currently online

LastActivityDate

The date when the user logged-in or was last authenticated. If the last login was persistent, this will not necessarily be the date of the login, but it may be the date when the user accessed the site and was automatically authenticated through the cookie.

LastLockoutDate

The date when the user was automatically locked-out by the membership system, after a (configurable) number of invalid logins

LastLoginDate

The date of the last login

LastPasswordChangedDate

When the user last changed his or her password

PasswordQuestion

The question asked of users who forget their password — used to prove it's really them

UserName

The user's username

Method

Description

ChangePassword

Changes the user's password. The current password must be provided.

ChangePasswordQuestionAndAnswer

Changes the question and answer asked of a user who forgets his or her password. Requires the current password as input (so someone can't change this for somebody else).

GetPassword

Returns the current password. Depending on how the membership system is set up, it may require the answer to the user's password question as input and will not work if only a password hash is stored in the database.

ResetPassword

Creates a new password for the user. This is the only function to change the password if the membership system was set up to hash the password.

UnlockUser

Unlocks the user if she was previously locked out by the system because of too many invalid attempts to log in.

When you change a user property, the new value is not immediately persisted to the data store; you have to call the UpdateUser method of the Membership class for that. This is done so that with a single call you can save multiple updated properties, and thus improve performance.

By using these two classes together, you can completely manage the accounts' data in a very intuitive and straightforward way. It's outside the scope of this book to provide a more exhaustive coverage of every method and overload, but I can show you a few examples about their usage in practice — please consult MSDN for all the details on these classes. Following is some code for registering a new account and handling the exception that may be raised if an account with the specified username or e-mail address already exists:

string msg = "User created successfully!";
try
{
   MembershipUser newUser = Membership.CreateUser(
      "Marco", "secret", "mbellinaso@wrox.com");
}
catch (MembershipCreateUserException exc)
{
   msg = "Unable to create the user. ";
   switch (exc.StatusCode)
   {
      case MembershipCreateStatus.DuplicateEmail:
         msg += "An account with the specified e-mail already exists.";
         break;
      case MembershipCreateStatus.DuplicateUserName:
         msg += "An account with the specified username already exists.";
         break;
      case MembershipCreateStatus.InvalidEmail:
         msg += "The specified e-mail is not valid.";
         break;
      case MembershipCreateStatus.InvalidPassword:
         msg += "The specified password is not valid.";
         break;
      default:
         msg += exc.Message;
         break;
   }
}

lblResult.Text = msg;

If you want to change some of the user's information, you first retrieve a MembershipUser instance that represents that user, change some properties as desired, and then update the user, as shown below:

MembershipUser user = Membership.GetUser("Marco");
if (DateTime.Now.Subtract(user.LastActivityDate).TotalHours < 2)
    user.Comment = "very knowledgeable user; strong forum participation!";
Membership.UpdateUser(user);

Validating user credentials from a custom login form requires only a single line of code (and not even that, as you'll see shortly):

bool isValid = Membership.ValidateUser("Marco", "secret");

In the "Solution" section of this chapter, you will use these classes to implement the following features in the site's Administration area:

  • Retrieve the total number of users and determine how many of them are currently online.

  • Find users by partial username or e-mail address.

  • Display some information about the users returned by the search, listed in a grid, such as the date of the user's last activity and whether they are active or not. In another page we will display all the details of a specific user and will allow the administrator to change some details.

The Provider Model Design Pattern

I use the term "data store" to refer to any physical means of persisting (saving) data — this usually means saving data in a database or in Active Directory, but .NET abstracts the actual data storage mechanism from the classes that manipulate the data. The provider class is the one that stores the data on behalf of other classes that manipulate data. This provider model design pattern, introduced in Chapter 3, is pervasive in .NET 2.0 — you can frequently "plug in" a different back-end provider to change the mechanism used to save and retrieve data. The Membership class uses a secondary class (called a membership provider) that actually knows the details of a particular data store and implements all the supporting logic to read and write data to/from it. You can almost think of the Membership class as a business layer class (in that it only manipulates data), and the provider class as the data access class which provides the details of persistence (even though a pure architect might argue the semantics). Two built-in providers are available for the Membership system, and you can choose one by writing some settings in the web.config file. The built-in providers are the ones for SQL Server 2000/2005 (SqlMembershipProvider) and for Active Directory (ActiveDirectoryMembershipProvider), but you can also write your own or find one from a third party (for use with Oracle, MySQL, DB2, etc., or perhaps XML files). Figure 4-1 provides a visual representation of the provider model design pattern.

Image from book
Figure 4-1

I find that the use of the provider model provides tremendous flexibility, because you can change the provider used by the Membership API under the hood without affecting the rest of the code, because you just access the Membership "business" class from the pages and the other business classes, and not the providers directly. Actually, you may even ignore which provider is used, and where and how the data is stored (this is the idea behind abstraction of the data store). Abstraction is obviously provided to users in the sense that they don't need to know exactly how their data will be stored, but now we also have abstraction for developers because they, too, don't always need to know how the data is stored!

To create a new provider you can either start from scratch by building a completely new provider that inherits directly from System.Web.Security.MembershipProvider (which in turn inherits from System.Configuration.Provider.ProviderBase) or you can just customize the way some methods of an existing provider work. For example, let's assume you want to modify the SqlMembershipProvider so it validates a user's password to make sure that it's not equal to his username. You simply need to define your own class, which inherits from SqlMembershipProvider, and you can just override the CreateUser method like this:

class SqlMembershipProviderEx : SqlMembershipProvider
{
   public override MembershipUser CreateUser(
      string username, string password, string email,
      string passwordQuestion, string passwordAnswer, bool isApproved,
      object providerUserKey, out MembershipCreateStatus status)
   {
      if (username.ToLower() == password.ToLower())
      {
         status = MembershipCreateStatus.InvalidPassword;
         return null;
      }

      else
      {
         return base.CreateUser(username, password, email,
            passwordQuestion, passwordAnswer, isApproved,
            providerUserKey, out status);
      }
   }
}

Important 

The provider model design pattern is also very useful in the migration of legacy systems that already use their own custom tables and stored procedures. Your legacy database may already contain thousands of records of user information, and you want to avoid losing them, but now you want to modify your site to take advantage of the new Membership class. Instead of creating a custom application to migrate data to a new data store (or using SQL Server DTS to copy the data from your tables to the new tables used by the standard SqlMembershipProvider), you can just create your own custom provider that directly utilizes your existing tables and stored procedures. If you're already using a business class to access your account's data from the ASP.NET pages, then creating a compliant provider class may be just a matter of changing the name and signature of some methods. Alternately, you can follow this approach: Keep your current business class intact, but make it private, and then move it inside a new provider class that delegates the implementation of all its methods and properties to that newly private legacy business class. The advantage of doing this instead of just using your current business class "as is" is that you can change to a different data store later by just plugging it into the membership infrastructure — you wouldn't have to change anything in the ASP.NET pages that call the built-in Membership class.

Once you have the provider you want (either one of the default providers, a custom one you developed on your own, or a third-party offering) you have to tell ASP.NET which one you want to use when you call the Membership class' methods.

The web.config file is used to specify and configure the provider for the Membership system. Many of the default configuration settings are hard-coded in the ASP.NET runtime instead of being saved into the Machine.Config file. This is done to improve performance by reading and parsing a smaller XML file when the application starts, but you can still modify these settings for each application by assigning your own values in web.config to override the defaults. You can read the default settings by looking at the Machine.config.default file found in the following folder (the "xxxxx" part should be replaced with the build number of your installation):

C:\<Windows Folder>\Microsoft.NET\Framework\v2.0.xxxxx\CONFIG

What follows is the definition of the <membership> section of the file, where the SqlMembershipProvider is specified and configured:

<system.web>
   <membership>
      <providers>
         <add name="AspNetSqlMembershipProvider"

            type="System.Web.Security.SqlMembershipProvider, System.Web,
               Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
            connectionStringName="LocalSqlServer"
            enablePasswordRetrieval="false"
            enablePasswordReset="true"
            requiresQuestionAndAnswer="true"
            applicationName="/"
            requiresUniqueEmail="false"
            passwordFormat="Hashed"
            maxInvalidPasswordAttempts="5"
            passwordAttemptWindow="10"
            passwordStrengthRegularExpression=""
         />
      </providers>
   </membership>

   <!-- other settings... -->
</system.web>

You can register more providers inside the <providers> section, and choose which one you want to use by specifying its name in the defaultProvider attribute of the <membership> element (not shown above). Another attribute of <membership> is userIsOnlineTimeWindow, which specifies how many minutes after the last activity a user is still considered online. That is, if a user logs in, brings up one page, but then closes her browser immediately, she will be counted as being online for this number of minutes. We need this kind of parameter because we have no definite way to know when a user has left the site or closed down their browser. You can test this by checking the value returned by Membership.GetNumberOfUsersOnline as users come to your site and then leave.

For our site we will use SQL Server 2005 Express Edition, the free edition of SQL Server 2005, to store the accounts' credentials, and this database will also be used for all the dynamic content of the site. In the "Solution" section of this chapter, you'll see in practice how to add and configure the provider for this data store. Although this database edition is adequate for use on a developer's computer, it would be wise to use a more feature-laden edition of SQL Server 2005 for production deployment to get better development and analysis tools, and to get the best performance from high-end server computers.

More Details About SqlMembershipProvider

In the last code snippet, you saw the default settings used to register the SqlMembershipProvider. The following table lists the attributes you can specify when you register the provider, in the <provider> element.

Attribute

Description

applicationName

The name of the web application; used if you want to store data on user account's for multiple web sites in a single database

connectionStringName

The name of the connection string, registered in the <connectionStrings> section of web.config, that points to the SQL Server database used to store the data.

Important: This is not the actual connection string! This is only a name that refers to web.config, where the actual connection string is stored.

description

A description for the provider

enablePasswordReset

Indicates whether you want to enable the methods and controls for resetting a password to a new, auto-generated one

enablePasswordRetrieval

Indicates whether you want to enable the methods and controls that allow a user to retrieve her forgotten password

maxInvalidPasswordAttempts

The maximum number of invalid login attempts. If the user fails to log in after this number of times, within the number of minutes specified by the passwordAttemptWindow attribute, the user account is "locked out" until the administrator explicitly calls the UnlockUser method of a MembershipUser instance representing the specific user.

minRequiredNonalphanumeric Characters

The minimum number of non-alphanumeric characters a password must have to be valid

minRequiredPasswordLength

The minimum number of characters for a valid password

name

The name used to register the provider. This is used to choose the provider by setting the defaultProvider attribute of the <membership> element.

passwordAttemptWindow

The number of minutes used to time invalid login attempts. See the description for maxInvalidPasswordAttempts.

passwordFormat

Specifies how the password is stored in the data store. Possible values are Clear, Encrypted, and Hashed.

passwordStrengthRegular Expression

The regular expression that a password must match to be considered valid

requiresQuestionAndAnswer

Indicates whether the user must respond to a personal secret question before retrieving or resetting her password. Questions and answers are chosen by users at registration time.

requiresUniqueEmail

Indicates whether the same e-mail address can be used to create multiple user accounts

Note 

By default, minRequiredPasswordLength is set to 7 and minRequiredNonalphanumericCharacters is set to 1, meaning that you must register with a password that is at least seven characters long and contains at least one non-alphanumeric character. Whether you leave these at their default settings or change them to suit your needs, remember to list these values on your registration page to let users know your password requirements.

These attributes let you fine-tune the membership system. For example, the ability to specify a regular expression that the password must match gives you great flexibility to meet stringent requirements. But one of the most important properties is certainly passwordFormat, used to specify whether you want passwords to be encrypted, or whether you just want a hash of them saved. Passwords are hashed or encrypted using the key information supplied in the <machineKey> element of the configuration file (you should remember to synchronize this machine key between servers if you will deploy to a server farm). The default algorithm used to calculate the password's hash is SHA1, but you can change it through the validation attribute of the machineKey element. Storing passwords in clear text offers the best performance when saving and retrieving the passwords, but it's the least secure solution. Encrypting a password adds some processing overhead, but it can greatly improve security. Hashing passwords provides the best security because the hashing algorithm is one way, which means the passwords cannot be retrieved in any way, even by an administrator. If a user forgets her password, she can only reset it to a new auto-generated one (typically sent by e-mail to the user). The best option always depends on the needs of each particular web site: If I were saving passwords for an e-commerce site on which I might also save user credit card information, I would surely hash the password and use a SSL connection in order to have the strongest security. For our content-based web site, however, I find that encrypting passwords is a good compromise. It's true that we're also building a small e-commerce store, but we're not going to store very critical information (credit cards numbers or other sensitive data) on our site.

Important 

Never store passwords in clear text. The small processing overhead necessary to encrypt and decrypt passwords is definitely worth the increased security, and thus the confidence that the users and investors have in the site.

Exploring the Default SQL Server Data Store

Even though the ASP.NET 2.0 membership system is pre-built and ready to go, this is not a good reason to ignore its design and data structures. You should be familiar with this system to help you diagnose any problems that might arise during development or deployment. Figure 4-2 shows the tables used by the SqlMembershipProvider class to store credentials and other user data. Of course, the data store's design of other providers may be completely different (especially if they are not based on relational databases).

Image from book
Figure 4-2

The interesting thing you can see from Figure 4-2 is the presence of the aspnet_Applications table, which contains a reference to multiple applications (web sites). Both the aspnet_Users table and the aspnet_Membership table contain a reference to a record in aspnet_Applications through the ApplicationId foreign key. This design enables you to use the same database to store user accounts for multiple sites, which can be very helpful if you have several sites using the same database server (commonly done with corporate web sites or with commercial low-cost shared hosting). In a situation where you have a critical application that requires the maximum security, you'll want to store the membership data in a dedicated database that only the site administrator can access. In our case, however, we're only using a SQL Server 2005 Express Edition database, and this requires us to use our own private database, deployed as a simple file under the App_Data special folder. In addition to these tables, there are also a couple of views related to membership (vw_aspnet_MembershipUsers and vw_aspnet_Users) and a number of stored procedures (aspnet_membership_xxx) for the CRUD (Create, Read, Update, and Delete) operations used for authorization. You can explore all these objects by using the Server Explorer window integrated within the Visual Studio's IDE, as shown in Figure 4-3.

Image from book
Figure 4-3

If you configure the provider so that it uses the default SQL Server Express database named ASPNETDB located under the App_Data folder, the ASP.NET runtime will automatically create all these database objects when the application is run for the first time! Because we are using this database for our site, we don't need to do anything else to set up the data store. However, if you're using SQL Server 2000 or a full edition of SQL Server 2005, you'll need to set up the tables manually by running the aspnet_sql.exe tool from the Visual Studio 2005 command prompt. This little program lets you to choose an existing database on a specified server, and it creates all the required objects to support membership, along with caching, profiles, personalization, and more. Figure 4-4 displays a couple of screens generated by this tool.

Image from book
Figure 4-4

The Graphical Login Controls

As you saw earlier, creating, validating, and managing users programmatically requires only a few lines of code. But what about writing no code at all? That's actually possible now, thanks to the new Login family of controls introduced with ASP.NET 2.0. These controls provide a pre-made user interface for the most common operations dealing with membership and security, such as creating a new account, logging in and out, retrieving or resetting a forgotten password, or showing different output according to the authenticated status of the current user. In Figure 4-5 you see a screenshot of the Visual Studio 2005 IDE, which shows the section of the Toolbox with the Login controls. It also shows a CreateUserWizard control dropped on a form, and its Smart Tasks pop-up window.

Image from book
Figure 4-5
Important 

The Smart Tasks window is a new feature of Visual Studio 2005's Web Form Designer. It contains links to the given control's most common customizable settings and configuration options. It is usually opened automatically as soon as you drop the control on the form, but you can also open and close it later, by clicking on the small arrow icon that shows up on the top-right corner when the control is selected.

The CreateUserWizard Control

A wizard is a new feature in ASP.NET 2.0 used to create a visual interface for a process that involves multiple steps. Each step has a separate visual panel or frame containing its own group of controls. After the user fills in values for controls of each step, he can press a link to advance to the next step in the wizard.

The CreateUserWizard control creates a user interface for a user to register, by providing the username, password, and e-mail address. The secret question and answer are also requested, but only if the current membership provider has the requiresQuestionAndAnswer attribute set to true; otherwise, these two last textboxes are hidden. When the Submit button is clicked, the control calls Membership.CreateUser under the hood on your behalf. By default, the code produced by the designer (and visible in the Source View) is as follows:

<asp:CreateUserWizard ID="CreateUserWizard1" runat="server">
   <WizardSteps>
      <asp:CreateUserWizardStep runat="server">
      </asp:CreateUserWizardStep>
      <asp:CompleteWizardStep runat="server">
      </asp:CompleteWizardStep>
   </WizardSteps>
</asp:CreateUserWizard>

It contains no appearance attributes, and the control will look plain and simple, with the default font, background, and foreground colors. However, you can specify values for all the attributes used to control the appearance. An easy way to do that is by clicking Auto Format from the Smart Tasks window and selecting one of the pre-made styles. For example, if you select the Elegant style, the control will look as shown in Figure 4-6.

Image from book
Figure 4-6

The corresponding source code was automatically updated as follows:

<asp:CreateUserWizard ID="CreateUserWizard1" runat="server"
   BackColor="#F7F7DE" BorderColor="#CCCC99" BorderStyle="Solid"
   BorderWidth="1px" Font-Names="Verdana" Font-Size="10pt">
   <WizardSteps>
      <asp:CreateUserWizardStep runat="server">
      </asp:CreateUserWizardStep>
      <asp:CompleteWizardStep runat="server">
      </asp:CompleteWizardStep>
   </WizardSteps>
   <SideBarStyle BackColor="#7C6F57" BorderWidth="0px"
      Font-Size="0.9em" VerticalAlign="Top" />
   <SideBarButtonStyle BorderWidth="0px" Font-Names="Verdana"
      ForeColor="#FFFFFF" />
   <NavigationButtonStyle BackColor="#FFFBFF" BorderColor="#CCCCCC"
      BorderStyle="Solid" BorderWidth="1px" Font-Names="Verdana"
      ForeColor="#284775" />
   <HeaderStyle BackColor="#F7F7DE" BorderStyle="Solid" Font-Bold="True"
      Font-Size="0.9em" ForeColor="#FFFFFF" HorizontalAlign="Left" />
   <CreateUserButtonStyle BackColor="#FFFBFF" BorderColor="#CCCCCC"
      BorderStyle="Solid" BorderWidth="1px" Font-Names="Verdana"
      ForeColor="#284775" />
   <ContinueButtonStyle BackColor="#FFFBFF" BorderColor="#CCCCCC"

      BorderStyle="Solid" BorderWidth="1px" Font-Names="Verdana"
      ForeColor="#284775" />
   <StepStyle BorderWidth="0px" />
   <TitleTextStyle BackColor="#6B696B" Font-Bold="True" ForeColor="#FFFFFF" />
</asp:CreateUserWizard>

Back in Chapter 2, we discussed the disadvantages of having the appearance properties set in the .aspx source files, in comparison to having them defined in a separate skin file as part of an ASP.NET 2.0 Theme. Therefore, I strongly suggest that you not leave the auto-generated appearance attributes in the page's source code, but instead cut-and-paste it into a skin file. You can paste everything except for the ID property and the <WizardSteps> section, as they are not part of the control's appearance.

The <WizardSteps> section lists all the steps of the wizard. By default it includes the step with the registration form, and a second one with the confirmation message. You can add other steps between these two, and in our implementation we'll add a step immediately after the registration form, where the user can associate a profile to the new account. The wizard will automatically provide the buttons for moving to the next or previous step, or to finish the wizard, and raise a number of events to notify your program of what is happening, such as ActiveStepChanged, CancelButtonClick, ContinueButtonClick, FinishButtonClick, NextButtonClick, and PreviousButtonClick.

If the available style properties are not enough for you, and you want to change the structure of the control, i.e., how the controls are laid down on the form, you can do that by defining your own template for the CreateUserWizardStep (or for the CompleteWizardStep). As long as you create the textboxes with the IDs the control expects to find, the control will continue to work without requiring you to write code to perform the registration manually. The best and easiest way to find which ID to use for each control is to have Visual Studio .NET convert the step's default view to a template (click the Customize Create User Step link in the control's Smart Tasks window), and then modify the generated code as needed. The code that follows is the result of Visual Studio's conversion and the deletion of the HTML layout tables:

<WizardSteps>
   <asp:CreateUserWizardStep runat="server">
      <ContentTemplate>
      <b>Sign Up for Your New Account</b><p></p>
      User Name: <asp:TextBox ID="UserName" runat="server" />
      <asp:RequiredFieldValidator ID="UserNameRequired" runat="server"
         ControlToValidate="UserName" ErrorMessage="User Name is required."
         ValidationGroup="CreateUserWizard1">*</asp:RequiredFieldValidator>
      <br />
      Password: <asp:TextBox ID="Password" runat="server" TextMode="Password" />
      <asp:RequiredFieldValidator ID="PasswordRequired" runat="server"
         ControlToValidate="Password" ErrorMessage="Password is required."
         ValidationGroup="CreateUserWizard1">*</asp:RequiredFieldValidator>
      <br />
      Confirm Password: <asp:TextBox ID="ConfirmPassword" runat="server"
         TextMode="Password" />
      <asp:RequiredFieldValidator ID="ConfirmPasswordRequired" runat="server"
         ControlToValidate="ConfirmPassword"
         ErrorMessage="Confirm Password is required."
         ValidationGroup="CreateUserWizard1">*</asp:RequiredFieldValidator>
      <asp:CompareValidator ID="PasswordCompare" runat="server"
         ControlToCompare="Password" ControlToValidate="ConfirmPassword"
         ErrorMessage="The Password and Confirmation Password must match."

         ValidationGroup="CreateUserWizard1"></asp:CompareValidator>
      <br />
      E-mail: <asp:TextBox ID="Email" runat="server" />
      <asp:RequiredFieldValidator ID="EmailRequired" runat="server"
         ControlToValidate="Email" ErrorMessage="E-mail is required."
         ValidationGroup="CreateUserWizard1">*</asp:RequiredFieldValidator>
      <br />
      Security Question: <asp:TextBox ID="Question" runat="server" />
      <asp:RequiredFieldValidator ID="QuestionRequired" runat="server"
         ControlToValidate="Question" ErrorMessage="Security question is required."
         ValidationGroup="CreateUserWizard1">*</asp:RequiredFieldValidator>
      <br />
      Security Answer: <asp:TextBox ID="Answer" runat="server" />
      <asp:RequiredFieldValidator ID="AnswerRequired" runat="server"
         ControlToValidate="Answer" ErrorMessage="Security answer is required."
         ValidationGroup="CreateUserWizard1">*</asp:RequiredFieldValidator>
      <br />
      <asp:Literal ID="ErrorMessage" runat="server" EnableViewState="False" />
      </ContentTemplate>
   </asp:CreateUserWizardStep>
   <asp:CompleteWizardStep runat="server">
   </asp:CompleteWizardStep>
</WizardSteps>

Important 

If you pay attention to the declaration of the various validation controls, you will note that all of them have a ValidationGroup property set to the control's name, i.e., CreateUserWizard1. The CreateUserWizard creates a Submit button with the same property, set to the same value. When that button is clicked, only those validators that have the ValidationGroup property set to the same value will be considered. This is a powerful new feature of ASP.NET 2.0 that you can use anywhere to create different logical forms for which the validation is run separately, according to which button is clicked.

Note 

Note that for the custom-made Create User step of the wizard, the Question and Password fields are not automatically hidden if the current membership provider has the requiresQuestionAndAnswer attribute set to false, as would happen otherwise.

You can also set up the control so that it automatically sends a confirmation e-mail to users when they complete the registration process successfully. The setup is defined by the CreateUserWizard's <MailDefinition> subsection, and consists of the sender's e-mail address, the mail subject, and a reference to the text file that contains the e-mail's body. The following code shows an example:

<asp:CreateUserWizard runat="server" ID="CreateUserWizard1">
   <WizardSteps>

      ...
   </WizardSteps>
   <MailDefinition
      BodyFileName="~/RegistrationMail.txt"
      From="yourname@yourserver.com"
      Subject="Mail subject here">
   </MailDefinition>
</asp:CreateUserWizard>

The RegistrationMail.txt file can contain the <% UserName %> and <% Password %> special placeholders, which at runtime will be replaced with the values taken from the new registration's data. To send the mail, you must have configured the SMTP server settings in the web.config file, through the <mailSettings> element and its sub-elements, as shown in the following code:

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
   <system.web> <!-- some settings here...--> </system.web>
   <system.net>
      <mailSettings>
         <smtp deliveryMethod="Network" from="thebeerhouse@wrox.com">
            <network defaultCredentials="true" host="(localhost)" port="25" />
         </smtp>
      </mailSettings>
   </system.net> </configuration>
The Login Control

The Login control does exactly what its name suggests: It allows the user to log in. It provides the user interface for typing the username and password, and choosing whether the login will be persistent (saved across different sessions) or not. For the default simple appearance, you just need to declare the control as follows:

<asp:Login ID="Login1" runat="server" />

However, if you apply the Elegant pre-built style to it, it will look as represented in Figure 4-7.

Image from book
Figure 4-7

Under the covers, this control calls the Membership.ValidateUser method to check whether the provided credentials are found in the data store, and if so, it calls FormsAuthentication .RedirectFormLoginPage to create the encrypted authentication ticket, saves it into a client cookie, and redirects to the page that the user originally tried to access before being redirected to the login page. The control exposes a lot of properties: Many are for changing its appearance (colors, fonts, etc.), and others enable you to specify whether you want to show a link to the registration page (CreateUserText and CreateUserUrl properties), a link to the page to recover a forgotten password (PasswordRecoveryText and PasswordRecoveryUrl properties), and whether the control should be hidden when the user is already logged in (the VisibleWhenLoggedIn property). Of course, as for the CreateUserWizard control, you can completely customize the way the control looks, by defining a template. Here's an example:

<asp:Login ID="Login1" runat="server">
   <LayoutTemplate>
      Username: <asp:TextBox ID="UserName" runat="server" />
      <asp:RequiredFieldValidator ID="UserNameRequired" runat="server"
         ControlToValidate="UserName" ErrorMessage="User Name is required."

         ValidationGroup="Login1">*</asp:RequiredFieldValidator>
      Password: <asp:TextBox ID="Password" runat="server" TextMode="Password" />
      <asp:RequiredFieldValidator ID="PasswordRequired" runat="server"
         ControlToValidate="Password" ErrorMessage="Password is required."
         ValidationGroup="Login1">*</asp:RequiredFieldValidator>
      <asp:CheckBox ID="RememberMe" runat="server" Text="Remember me next time." />
      <asp:Literal ID="FailureText" runat="server" EnableViewState="False" />
      <asp:Button ID="LoginButton" runat="server" CommandName="Login"
      Text="Log In" ValidationGroup="Login1" />
   </LayoutTemplate>
</asp:Login>

Remember that the only important thing is that you give textboxes, buttons, labels, and other controls the specific IDs that the parent control expects to find. If you start defining the template from the default template created by VS2005, this will be very easy.

The ChangePassword Control

The ChangePassword control allows users to change their current password, through the user interface shown in Figure 4-8.

Image from book
Figure 4-8

This control is completely customizable in appearance, by means of either properties or a new template. As with the CreateUserWizard control, its declaration can contain a <MailDefinition> section where you can configure the control to send a confirmation e-mail to the user with her new credentials.

The PasswordRecovery Control

The ChangePassword control enables users to recover or reset their password, in case they forgot it. The first step, represented in Figure 4-9, is to provide the username.

Image from book
Figure 4-9

For the next step, the user will be asked the question he or she chose at registration time. If the answer is correct, the control sends the user an e-mail message. As expected, there must be the usual MailDefinition, along with the current password, or a newly generated one if the membership provider's enable PasswordRetrieval attribute is set to false, or if the provider's passwordFormat is hashed.

The LoginStatus, LoginName, and LoginView Controls

These last three controls are the simplest ones, and are often used together. The LoginName control shows the name of the current user. It has a FormatString property that can be used to show the username as part of a longer string, such as "Welcome {0}!", where the username will replace the {0} placeholder. If the current user is not authenticated, the control shows nothing, regardless of the FormatString value.

The LoginStatus control shows a link to log out or log in, according to whether the current user is or is not authenticated. The text of the links can be changed by means of the LoginText and LogoutText properties, or you can use graphical images instead of plain text, by means of the LoginImageUrl and LogoutImageUrl properties. When the Login link is clicked, it redirects to the login page specified in the web.config file's <forms> element, or to the Login.aspx page if the setting is not present. When the Logout link is clicked, the control calls FormsAuthentication.SignOut to remove the client's authentication ticket, and then can either refresh the current page or redirect to a different one according to the values of the LogoutAction and LogoutPageUrl properties.

The LoginView allows you to show different output according to whether the current user is authenticated. Its declaration contains two subsections, <AnonymousTemplate> and <LoggedInTemplate>, where you place the HTML or ASP.NET controls that you want to display when the user is anonymous (not logged in) or logged in, respectively. The code that follows shows how to display the login control if the user is not authenticated yet, or a welcome message and a link to log out otherwise:

<asp:LoginView ID="LoginView1" runat="server">
   <AnonymousTemplate>
      <asp:Login runat="server" ID="Login1" />
   </AnonymousTemplate>
   <LoggedInTemplate>
      <asp:LoginName ID="LoginName1" runat="server" FormatString="Welcome {0}" />
      <br />
      <asp:LoginStatus ID="LoginStatus1" runat="server" />
   </LoggedInTemplate>
</asp:LoginView>

Setting Up and Using Roles

An authentication/authorization system is not complete without support for roles. Roles are used to group users together for the purpose of assigning a set of permissions, or authorizations. You could decide to control authorizations separately for each user, but that would be an administrative nightmare! Instead, it's helpful to assign a user to a predetermined role and give him the permissions that accompany the role. For example, you can define an Administrator's role to control access to the restricted pages used to add, edit, and delete the site's content, and only users who belong to the Administrators role will be able to post new articles and news. It is also possible to assign more than one role to a given user. In ASP.NET 1.x, there wasn't any built-in support for roles in forms authentication — roles were only supported with Windows security. You could have added role support manually (as shown in the first edition of this book), but that required you to create your own database tables and write code to retrieve the roles when the user logged in. You could have written the code so that the roles were retrieved at runtime — and then encrypted together by the authentication ticket on a client's cookie — so that they were not retrieved separately from the database with each request. Besides taking a considerable amount of development time that you could have spent adding value to your site, it was also a crucial task: Any design or implementation bugs could impact performance, or even worse, introduce serious security holes. The good news is that ASP.NET 2.0 has built-in support for roles, and it does it the right way with regard to performance, security, and flexibility. In fact, as is true in many other pieces of ASP.NET 2.0 (membership, sessions, profiles, personalization), it is built on the provider model design pattern: A provider for SQL Server is provided, but if you don't like some aspect of how it works, or you want to use a different data store, you can write your own custom provider or acquire one from a third party.

The role management is disabled by default to improve performance for sites that don't need roles — role support requires the execution of database queries, and consequent network traffic between the database server and the web server. You can enable it by means of the <roleManager> element in the web.config file, as shown here:

<roleManager enabled="true" cacheRolesInCookie="true" cookieName="TBHROLES" />

This element allows you to enable roles and configure some options. For example, the preceding code enables role caching in the client's cookie (instead of retrieving them from the database on each web request), which is a suggested best practice. Unless specified otherwise, the default provider will be used, with a connection string to the default local SQL Server Express database (the ASPNETDB file under the App_Data folder). If you want to use a different database, just register a new provider within the <roleManager> element, and choose it by setting the roleManager's defaultProvider attribute.

System.Web.Security.Roles is the class that allows you to access and manage role information programmatically. It exposes several static methods, the most important of which are listed in the following table.

Method

Description

AddUserToRole, AddUserToRoles, AddUsersToRole, AddUsersToRoles

Adds one or more users to one or more roles

CreateRole

Creates a new role with the specified name

DeleteRole

Deletes an existing role

FindUsersInRole

Finds all users who belong to the specified role, and who have a username that matches the input string. If the default provider for SQL server is used, the username can contain any wildcard characters supported by SQL Server in LIKE clauses, such as % for any string of zero or more characters, or _ for a single character.

GetAllRoles

Returns an array with all the roles

GetRolesForUser

Returns an array with all the roles to which the specified user belongs

GetUsersInRole

Returns the array of usernames (not MembershipUser instances) of users who belong to the specified role

IsUserInRole

Indicates whether the specified user is a member of the specified role

RemoveUserFromRole, RemoveUserFromRoles, RemoveUsersFromRole, RemoveUsersFromRoles

Removes one or more users from one or more roles

RoleExists

Indicates whether a role with the specified name already exists

Using these methods is straightforward, and you will see some practical examples in the "Solution" section of this chapter, where we implement the administration console to add/remove users to and from roles.

Important 

The roles system integrates perfectly with the standard IPrincipal security interface, which is implemented by the object returned by the page's User property. Therefore, you can use the User object's IsInRole method to check whether the current user belongs to the specified role.

The SQL Server provider retrieves and stores the data from/to tables aspnet_Roles and aspnet_UsersInRoles. The latter links a user of the aspnet_Users table (or another user table, if you're using a custom membership provider for a custom database) to a role of the aspnet_Roles table. Figure 4-10 shows the database diagram again, updated with the addition of these two tables.

Image from book
Figure 4-10

Using Roles to Protect Pages and Functions Against Unauthorized Access

Basically, you have two ways to control and protect access to sensitive pages: You can do it either imperatively (programmatically) or declaratively (using a config file). If you want to do it by code, in the page's Load event you would write something like the following snippet:

if (!Roles.IsUserInRole("Administrators"))
{
   throw new System.Security.SecurityException(
      "Sorry, this is a restricted function you are not authorized to perform");
}

When you don't pass the username to the Roles.IsUserInRole method, it takes the name of the current user, and then "forwards" the call to the IsInRole method of the current user's IPrincipal interface. Therefore, you can call it directly and save some overhead using the following code:

if (!this.User.IsInRole("Administrators"))
{
    throw new System.Security.SecurityException(
       "Sorry, this is a restricted function you are not authorized to perform");
}

Note 

When Roles.IsUserInRole is called with the overload that takes in the username (which is not necessarily equal to the current user's username), the check is done by the selected role's provider. In the case of the built-in SqlRoleProvider, a call to the aspnet_UsersInRoles_IsUserInRole stored procedure is made.

The biggest disadvantage of imperative (programmatic) security is that in order to secure an entire folder, you have to copy and paste this code in multiple pages (or use a common base class for them). Even worse, when you want to change the ACL (access control list) for a page or folder (because, for example, you want to allow access to a newly created role), you will need to change the code in all those files! Declarative security makes this job much easier: you define an <authorization> section in a web.config (either for the overall site or for a subfolder), which specifies the users and roles who are allowed to access a certain folder or page. The following snippet of web.config gives access to members of the Administrators role, while everyone else (*) is denied access to the current folder's pages:

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
   <system.web>
      <authorization>
         <allow roles="Administrators"/>
         <deny users="*" />
      </authorization>
      <!-- other settings... -->
   </system.web>
</configuration>

Authorization conditions are evaluated from top to bottom, and the first one that matches the user or role stops the validation process. This means that if you switched the two conditions above, the <deny> condition would match for any user, and the second condition would never be considered; as a result, nobody could access the pages. This next example allows everybody except anonymous users (those who have not logged in and who are identified by the ? character):

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

If you want to have different ACLs for different folders, you can have a different <authorization> section in each folder's web.config file. As an alternative, you can place all ACLs in the root web.config, within different <location> sections, such as in this code:

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
   <system.web>
      <!-- settings for the current folder -->
      <authorization>
         <allow users="*" />
      </authorization>
   </system.web>

   <location path="Admin">
      <!-- settings for the Admin sub-folder -->
      <system.web>
         <authorization>
            <allow roles="Administrators" />
            <deny users="*" />
         </authorization>
      </system.web>
   </location>

   <location path="Members">
      <!-- settings for the Members sub-folder -->
      <system.web>
         <authorization>
            <deny users="?" />
            <allow users="*" />
         </authorization>
      </system.web>
   </location>
</configuration>

The path attribute of the <location> section can be the name of a subfolder (as shown above) or the virtual path of a single page. Using the <location> section is the only way to declaratively assign a different ACL to specific individual pages, since you can't have page-level web.config files. Although it's possible to restrict individual pages, it's more common to restrict entire subfolders.

Programmatic security checks are still useful and necessary in some cases, though, such as when you want to allow everybody to load a page, but control the visibility of some visual controls (e.g., buttons to delete a record, or a link to the administration section) of that page to specific users. In these cases you can use the code presented earlier to show, or hide, some server-side controls or containers (such as Panel) according to the result of a call to User.IsInRole. Alternatively, you can use a LoginView control that, in addition to its sections for anonymous and logged-in users, can also define template sections visible only to users who belong to specific roles. The next snippet produces different output according to whether the current user is anonymous, is logged in as a regular member, or is logged in and belongs to the Administrators role:

<asp:LoginView ID="LoginView1" runat="server">
   <AnonymousTemplate>anonymous user</AnonymousTemplate>
   <LoggedInTemplate>member</LoggedInTemplate>

   <RoleGroups>
      <asp:RoleGroup Roles="Administrators">
         <ContentTemplate>administrator</ContentTemplate>
      </asp:RoleGroup>
   </RoleGroups>
</asp:LoginView>

Note that in cases where the currently logged-in user is also in the Administrators role, the LoginView control only outputs the content of the <Administrators> section, not that of the more general <LoggedInTemplate> section.

Finally, roles are also integrated with the site map, which lets you specify which roles will be able to see a particular link in the Menu or TreeView control that consumes the site map. This is a very powerful feature that makes it easy to show a user only the menu options he is actually allowed to access! For example, if you want the Admin link to be visible only to Administrators, here's how you define the map's node:

<siteMapNode title="Admin" url="~/Admin/Default.aspx" roles="Administrators">

However, to enable this to work you must also register a new provider for the SiteMap system (in the <siteMap> section of the web.config file), and set its securityTrimmingEnabled attribute to true. Registering the provider for the site map is very similar to registering a provider for the membership or roles system; in the "Solution" section you will see code examples to illustrate this.

Setting Up and Using User Profiles

In the ASP.NET 1.x days, if you wanted to associate a profile to a registered user, you typically added a custom table to your database, or stored them together with the user credentials, in the same table. You also had to write quite a lot of code for the business and data access layers, to store, retrieve, and update that data from your web pages. ASP.NET 2.0 provides a built-in mechanism to manage user profiles, in an easy, yet very complete and flexible, way. This new feature can save you hours or even days of work! The Profile module takes care of everything — you just need to configure what the profile will contain, i.e., define the property name, type, and default value. This configuration is done in the root web .config file, within the <profile> section. The following snippet shows how to declare two properties, FavoriteTheme of type String, and BirthDate of type DateTime:

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
   <system.web>
      <profile>
         <properties>
            <add name="FavoriteTheme" type="String" />
            <add name="BirthDate" type="DateTime" />
         </properties>
      </profile>
   <!-- other settings... -->
   </system.web>
</configuration>

Amazingly, this is all you need to do to set up a profile structure! When the application is run, the ASP.NET runtime dynamically adds a Profile property to the Page class, which means you will not find such a property in the Object Browser at design time. The object returned is of type ProfileCommon (inherited from System.Web.Profile.ProfileBase); you will not find this class in the Object Browser either, or on the documentation, because this class is generated and compiled on-the-fly, according to the properties defined in the web.config file. The result is that you can just access the page's Profile property and read/write its subproperties. The following code demonstrates how to read the values of the current user's profile to show them on the page when it loads, and then updates them when a Submit button is clicked:

protected void Page_Load(object sender, EventArgs e)
{
   if (!this.IsPostBack)
   {
      ddlThemes.SelectedValue = this.Profile.FavoriteTheme;
      txtBirthDate.Text = this.Profile.BirthDate.ToShortDateString();
   }
}

protected void btnSubmit_Click(object sender, EventArgs e)
{
   this.Profile.FavoriteTheme = ddlThemes.SelectedValue;
   this.Profile.BirthDate = DateTime.Parse(txtBirthDate.Text);
}

Even though you can't see these properties in the Object Browser, Visual Studio .NET is smart enough to compile this class in the background when the web.config file is modified, so you get full IntelliSense in the IDE, just as if the Profile properties were built-in properties of the Page class, like all the others. Figure 4-11 is a screenshot of the IDE with the IntelliSense in action.

Image from book
Figure 4-11
Important 

Having a class dynamically generated by Visual Studio 2005 with all the custom profile properties (and the IntelliSense for them) doesn't just speed up development, but also helps developers reduce inadvertent coding errors. In fact, this class provides strongly typed access to the user's profile, so if you try to assign a string or an integer to a property that expects a date, you'll get a compile-time error so you can correct the problem immediately!

When you define a profile property, you can also assign a default value to it, by means of the defaultValue attribute:

<add name="FavoriteTheme" type="String" defaultValue="Colorful" />

Note 

The default value for strings is an empty string, not null, as you may have thought. This makes it easier to read string properties, because you don't have to check whether they are null before using the value somewhere. The other data types have the same default values that a variable of the same type would have (e.g., zero for integers).

When you declare profile properties, you can also group them into subsections, as shown below:

<profile>
   <properties>
      <add name="FavoriteTheme" type="String" />
      <add name="BirthDate" type="DateTime" />
      <group name="Address">
         <add name="Street" type="String" />
         <add name="City" type="String" />
      </group>
   </properties>
</profile>

The Street property will be accessible as Profile.Address.Street. Note, however, that you can't define nested groups under each other, but can only have a single level of groups. If this limitation is not acceptable to you, you can define your own custom class with subcollections and properties, and reference it in the type attribute of a new property. In fact, you are not limited to base types for profile properties; you can also reference more complex classes (such as ArrayList or Color), and your own enumerations, structures, and classes, as long as they are serializable into a binary or XML format (the format is dictated by the property's serializeAs attribute).

Note 

The Profile system is built upon the provider model design pattern. ASP.NET 2.0 comes with a single built-in profile provider that uses a SQL Server database as a backing store. However, as usual, you can build your own providers or find them from third parties.

Accessing Profiles from Business Classes

Sometimes you may need to access the user's profile from a business class, or from a page base class. The Profile property is dynamically added only to the aspx pages' code-behind classes, so you can't use it in those situations. However, you can still access it through the Profile property exposed by the current HttpContext. The HttpContext class is the container for the current web request — it's used to pass around objects that are part of the request: forms, properties, ViewState, etc. Anytime you process a page, you will have this HttpContext information, so you can always pull a Profile class instance out of the HttpContext class. The returned type is ProfileBase, though, not the ProfileCommon object generated on-the-fly that enabled you to use IntelliSense and access properties in a strongly typed manner. Therefore, the resulting Profile class instance read from the HttpContext.Current .Profile will not be strongly typed. No problem — just do the cast and you are ready to use the profile as usual. Individual properties of the profile will be strongly typed, as expected. The following snippet shows a practical example:

ProfileCommon profile = HttpContext.Current.Profile as ProfileCommon;
profile.BirthDate = new DateTime(1980, 09, 28);

Accessing the Profile for Users Other Than the Current User

So far, all the examples have shown how to read and write the profile for the current user. However, you can also access other users' profiles — very useful if you want to implement an administration page to read and modify the profiles of your registered members. Your administrator must be able to read and edit the profile properties for any user. The ProfileCommon class exposes a GetProfile method that returns the profile for any specified user, and once you obtain this profile instance you can read and edit the profile properties just as you can do for the current user's profile. The only difference is that after changing some values of the retrieved profile, you must explicitly call its Save method, which is not required when you modify the profile for the current user (in the case of the current user, Save is called automatically by the runtime when the page unloads). Here's an example of getting a profile for a specified user, and then modifying a property value in that profile:

ProfileCommon profile = Profile.GetProfile("Marco");
profile.BirthDate = new DateTime(1980, 09, 28);
profile.Save();

Adding Support for Anonymous Users

The code shown above works only for registered users who are logged in. Sometimes, however, you want to be able to store profile values for users who are not logged in. You can explicitly enable the anonymous identification support by adding the following line to web.config:

<anonymousIdentification enabled="true"/>

After that, you must indicate what properties are available to anonymous users. By default, a property is only accessible for logged-in users, but you can change this by setting the property's allowAnonymous attribute to true, as follows:

<add name="FavoriteTheme" type="String"
   allowAnonymous="true" defaultValue="Colorful" />

This is useful to allow an anonymous user to select a theme for his current session. This would not be saved after his session terminates because we don't have an actual user identity to allow us to persist the settings. Another important concern regarding profiles for anonymous users is the migration from anonymous to authenticated status. Consider the following situation: A registered user comes to the site and browses it without logging in. He or she then changes some profile properties available to anonymous users, such as the name of the favorite theme. At some point he or she wants to access a restricted page and needs to log in. Now, because the favorite theme was selected while the user was anonymous, it was stored into a profile linked to an anonymous user ID. After the user logs in, he or she then becomes an authenticated user with a different user ID. Therefore, that user's previous profile settings are loaded, and the user will get a site with the theme selected during a previous session, or the default one. What you wanted to do, however, was to migrate the anonymous user's profile to the authenticated user's profile at the time he logged in. This can be done by means of the Profile_MigrateAnonymous global event, which you can handle in the Global.asax file. Once this event is raised, the HttpContext.Profile property will already have returned the authenticated user's profile, so it's too late for us to save the anonymous profile values. You can, however, get a reference to the anonymous profile previously used by the user, and then copy values from it to the new profile. In the "Solution" section you will see how to implement this event to avoid losing the user's preferences.

The Web Administration Tool

As you've seen thus far, the preferred method of setting up and configuring all the membership and profiling services introduced by ASP.NET 2.0 is to configure XML tags in the web.config file, which is the declarative coding method. To make your job even easier, you now have IntelliSense when you edit web.config in Visual Studio 2005, which wasn't supported in earlier versions, as you can see in Figure 4-12.

Image from book
Figure 4-12

However, the ASP.NET and VS2005 teams made things even simpler still by providing a web-based administration tool that you can launch by clicking the ASP.NET Configuration item from Visual Studio's Web Site Tool. This application provides help in the following areas:

  • Security: It enables you to set up the authentication mode (you can choose between the Intranet/Windows and the Internet/form-based model), create and manage users, create and manage roles, and create access rules for folders (you select a subfolder and declare which roles are granted or denied access to it). Figure 4-13 shows a couple of screenshots of these pages.

  • Application: It enables you to create and manage application settings (those inside the <appSettings> section), and configure the SMTP e-mail settings, debugging and tracing sections, and the default error page.

  • Provider: It enables you to select a provider for the Membership and the Roles systems. However, the providers must already be registered in web.config.

Image from book
Figure 4-13

These pages use the new configuration API to read and write sections and elements to and from the web.config file. This tool, however, is only intended to be used on the local server, not a remote site. If you want to administer these kinds of settings for a remote site (as we want to do for our site), you will need to modify the web pages for this tool, or design your own pages. Fortunately, the complete source code for the ASP.NET Administration Tool is available under C:\<Windows Folder>\ Microsoft.NET\Framework\v2.0.xxxxx\ASP.NETWebAdminFiles. You can go look at these pages to see how Microsoft implemented the features, and then you can do something similar for your own custom administration console.

Designing Our Solution

So far we have described the general features of the new membership and profile services introduced in ASP.NET 2.0, but we can now build upon this knowledge and design exactly how we can implement these features in our particular web site. Here's the summary of our design objectives regarding membership and profile features, and a description of the corresponding web pages:

  • A login box will be visible in the top-right corner of each page whenever the user is anonymous. After the user logs in, the login box will be hidden. Instead, we'll show a greeting message and links for Logout and Edit Profile.

  • A Register.aspx page will allow new users to register (create their own account), and we'll populate some profile settings upon registration. The profile will have the following first-level properties: FirstName (String), LastName (String), Gender (String), BirthDate (DateTime), Occupation (String), and Website (String). A profile group named Address will have the following subproperties: Street, PostCode, City, State, and Country, all of type String. Another group named Contacts will have the following string sub-properties: Phone and Fax. A final group named Preferences will have the Theme, Culture, and Newsletter properties. The Theme property is the only one that will be made available to anonymous users.

  • Our PasswordRecovery.aspx page will allow users to recover forgotten passwords; it can e-mail the password to the user's e-mail address that we have on file. This is possible because we'll configure the membership module to encrypt the password, instead of storing it as a hash (a hashed password is a one-way encryption that is not reversible). We had to decide whether we wanted the best possible security (hashing) or a more user-friendly encryption method that enables us to recover the user's password. In our scenario we've determined that the userfriendly option is the best choice.

  • Our EditProfile.aspx page will only be accessible to registered members, and it will allow them to change their account's password and the profile information they set up at registration time.

  • We'll create some administration pages to allow the administrator to read and edit all the information about registered users (members). A ManageUsers.aspx page will help the administrator look up records for members either by their username or e-mail address (searches by partial text will also be supported). Among the data returned will be their username, e-mail address, when they registered or last accessed the site, and whether they are active or not. A second page, EditUser.aspx, will show additional details about a single user, and will allow the administrator to enable or disable the account, assign new roles to the user, remove roles from the user, and edit the user's personal profile.


Previous Page
Next Page

Обзоры результатов матчей чемпионата англии по футболу, а также итоги тура.


JavaScript EditorFreeware javascript editor     Javascript code 
R7