If you want to set up a custom user registration system for your Web site, you can use Forms authentication. The advantage of this type of authentication is that it enables you to store usernames and passwords in whatever storage mechanism you desire . For example, you can store usernames and passwords in the Web.Config file, an XML file, or a database table. Forms authentication relies on browser cookies to determine the identity of a user. After you enable Forms authentication for a directory, pages in the directory cannot be accessed unless the user has the proper Authentication Ticket stored in a cookie. If a user requests a page without the proper Authentication Ticket, he or she can be automatically redirected to a login page. If the user enters a valid username and password combination, you can automatically redirect him or her back to the original page. When using Forms authentication, you can easily set up an automatic user registration system. For example, you can create a database table that contains usernames and passwords. In that case, adding a new registered user is as simple as adding a new username and password to the database table. The .NET classes for Forms authentication are located in the System.Web.Security namespace. The following list contains the most important of these classes:
You learn how to take advantage of these classes in the following sections. Enabling Forms AuthenticationTo enable basic Forms authentication for an application, you must complete the following three steps:
The first step is to enable Forms authentication for an application. To do so, you must modify an application's root directory Web.Config file. If the Web.Config file doesn't already exist, you can create it. CAUTION You must enable Forms authentication for an ASP.NET application as a whole. You can set the authentication mode only in the Web.Config file located in an application's root directory. However, enabling Forms authentication does not force you to password-protect every page. You can enable anonymous access for particular directories within an application even when Forms authentication is enabled for the application. NOTE All the files discussed in this section are located in the FormSimple directory on the CD that accompanies this book. To use these files, copy the FormSimple directory and all its subdirectories to your Web server. Next , create a new virtual directory that points to the FormSimple directory. The Web.Config file in Listing 19.1 contains the minimal amount of information necessary to enable Forms authentication for an application. Listing 19.1 FormsSimple/Web.Co nfig<configuration> <system.web> <authentication mode="Forms" /> </system.web> </configuration> The C# version of this code can be found on the CD-ROM. In Listing 19.1, the authentication mode is set to Forms . Creating this file enables Forms authentication for the entire application. CAUTION The names of the elements and attributes in the Web.Config file are case sensitive. The next step is to password-protect individual directories. You can require users to log in to access any ASP.NET page at your Web site by modifying the root directory Web.Config file. Alternatively, you can add Web.Config files to particular directories to password-protect only certain pages. To password-protect a particular directory and its subdirectories, add the Web.Config file in Listing 19.2 to the directory. (This file can be found in the FormsSimple\Secret subdirectory.) Listing 19.2 FormsSimple\Secret\Web.Config<configuration> <system.web> <authorization> <deny users="?" /> </authorization> </system.web> </configuration> The C# version of this code can be found on the CD-ROM. The Web.Config file in Listing 19.2 denies access to the ASP.NET pages contained in the directory to anonymous users. The ? symbol represents all anonymous users. CAUTION By default, Forms authentication applies only to ASP.NET pages. You cannot use it to password-protect other types of files, such as image files, Microsoft Word documents, text files, or Classic ASP files. If you want to use Forms authentication with other types of files, then you must explicitly map the file type to the aspnet_isapi.dll extension in the Internet Services Manager. After you add the Web.Config file in Listing 19.2 to a directory, anonymous users are denied access to ASP.NET pages in that directory and all subdirectories. If you want to enable users to access files in a particular subdirectory, you can add the Web.Config file contained in Listing 19.3. (This file can be found in the FormsSimple\Anon subdirectory.) Listing 19.3 FormsSimple\Anon\Web.Config<configuration> <system.web> <authorization> <allow users="?" /> </authorization> </system.web> </configuration> The C# version of this code can be found on the CD-ROM. The Web.Config file in Listing 19.3 allows all anonymous users to access any page contained in a directory and all the subdirectories of that directory. The final step required for enabling Forms authentication is to create the Login.aspx page. If you attempt to access an ASP.NET page in a password-protected directory and you don't have the proper Authentication Ticket cookie, you are automatically redirected to this page. The simple Login.aspx page contained in Listing 19.4 displays a form with a field for a username and password (see Figure 19.1). The page requires you to enter the username Sam and password Secret . (The form is case-sensitive.) Listing 19.4 FormsSimple\Login.aspx<Script Runat="Server"> Sub Button_Click( s As Object, e As EventArgs ) If IsValid Then If txtUsername.Text = "Sam" And txtPassword.Text = "Secret" Then FormsAuthentication.RedirectFromLoginPage( txtUsername.Text, chkRemember.Checked ) Else lblMessage.Text = "Bad username/password!" End If End If End Sub </Script> <html> <head><title>Login.aspx</title></head> <body> <form Runat="Server"> <h2>Please Login:</h2> <asp:Label ID="lblMessage" ForeColor="Red" Font-Bold="True" Runat="Server" /> <p> <b>Username:</b> <br> <asp:TextBox ID="txtUsername" Runat="Server" /> <asp:RequiredFieldValidator ControlToValidate="txtUsername" Text="Required!" Runat="Server" /> <p> <b>Password:</b> <br> <asp:TextBox ID="txtPassword" Runat="Server" /> <asp:RequiredFieldValidator ControlToValidate="txtPassword" Text="Required!" Runat="Server" /> <p> <asp:CheckBox ID="chkRemember" Runat="Server"/> Remember me with a cookie? <p> <asp:Button Text="Login!" OnClick="Button_Click" Runat="Server" /> </form> </body> </html> The C# version of this code can be found on the CD-ROM. Figure 19.1. A simple login page.
In Listing 19.4, the important work happens in the Button_Click subroutine, which first checks the IsValid property to test whether both a username and password were entered into the form. If the page is valid, the values of the username and password form fields are matched against the values Sam and Secret . If you enter the correct username and password, the RedirectFromLoginPage method is called. Two parameters are passed to this method: the username and a Boolean value indicating whether a persistent cookie should be created. Forms authentication supports both session and persistent cookies. When you call the RedirectFromLoginPage , you can indicate whether a persistent cookie should be created. If the RedirectFromLoginPage creates a persistent cookie, the cookie continues to exist even if the user shuts down his or her computer and returns to your Web site many days in the future. CAUTION Normally, you should allow users to decide whether to create a persistent cookie. For example, if you were temporarily using a computer at the library or a friend's house, you would not want to create a persistent cookie on that computer. Calling the RedirectFromLoginPage method performs two actions. First, it creates a cookie on the user's browser that contains an Authentication Ticket. After this cookie is set, the user can access pages in directories that require Forms authentication. The RedirectFromLoginPage method also automatically redirects the user back to the page that sent him or her to the Login.aspx page in the first place by using a browser redirect. NOTE The RedirectFromLoginPage method redirects the user back to the page indicated by the ReturnUrl query string variable. If the user links directly to the Login.aspx page, the ReturnUrl query string variable doesn't have a value. In that case, the RedirectFromLoginPage redirects the user to the Default.aspx page. Configuring Forms AuthenticationIn the preceding section, you learned how to modify the Web.Config file to enable Forms authentication for an application. In this section, you examine the options for configuring Forms authentication in more detail. The authentication section in the Web.Config file can contain an optional forms element, which supports the following attributes:
The protection attribute requires some explanation. By default, cookies are encrypted by using either DES or TripleDES encryption (depending on the capabilities of the server). Furthermore, the contents of the cookie are validated with a Message Authentication Code to protect against tampering. You can disable encryption or validation or both features by changing the value of the protection attribute. For example, setting protection to Encryption causes the cookie to be encrypted but not validated. You can get better performance from your application by disabling encryption and validation. However, disabling these features also results in a less secure site. The Web.Config file in Listing 19.5 illustrates how you can set the forms attributes. Listing 19.5 FormsAttributes\Web.Config<configuration> <system.web> <authentication mode="Forms"> <forms name=".MyCookie" loginUrl="/login/mylogin.aspx" protection="All" timeout="80" path="/"/> </authentication> </system.web> </configuration> The C# version of this code can be found on the CD-ROM. Configuring Forms AuthorizationThe authorization section of the Web.Config file determines which users can access ASP.NET pages within a directory. In the simplest case, you want to use the authorization section to deny all anonymous users access to the pages in a directory by using a Web.Config file like the one in Listing 19.6. Listing 19.6 Web.Config<configuration> <system.web> <authorization> <deny users="?" /> </authorization> </system.web> </configuration> The C# version of this code can be found on the CD-ROM. The authorization section can contain either <deny> elements, which deny access for particular users, or <allow> elements, which enable access for particular users. You can also use the special symbol ? , which stands for all anonymous users, or the symbol * , which stands for all users (both anonymous and authenticated). You can use the authorization section to make fine-grained distinctions among users and among different actions the users might perform. For example, suppose that you want to deny access to all anonymous users. Furthermore, you want to deny access to James and Mark even after they log in. You can do so by using the Web.Config file in Listing 19.7. Listing 19.7 Web.Config<configuration> <system.web> <authorization> <deny users="?" /> <deny users="James,Mark" /> </authorization> </system.web> </configuration> The C# version of this code can be found on the CD-ROM. In Listing 19.7, the <deny> element> denies access to James and Mark. So, even if these users log in through the Login.aspx page, they cannot access ASP.NET pages located in the current directory. Finally, you can use the verbs attribute to control whether users can use HTTP Post or HTTP Get . For example, the Web.Config file in Listing 19.8 enables anyone to get an ASP.NET page in the directory but prevents everyone except for James or Mark from posting an HTML form. Listing 19.8 Web.Config<configuration> <system.web> <authorization> <allow verbs="POST" users="James,Mark" /> <deny verbs="POST" users="*" /> <allow verbs="GET" users="*" /> </authorization> </system.web> </configuration> The C# version of this code can be found on the CD-ROM. Retrieving User InformationThe identity of a user authenticated with Forms authentication is represented by the FormsIdentity class. You can use the following properties of this class to retrieve information about an authenticated user:
NOTE You cannot retrieve one important bit of informationthe password associated with the user. There's a good reason for this limitation. The password is not actually stored as part of the cookie. If you want the password, you must look it up yourself. You can use the IsAuthenticated property to test whether this user has already been authenticated. If a user requests a page from a directory that requires authentication and then requests a page from a directory that does not require authentication, the IsAuthenticated property continues to return the value True . The Name property returns the name associated with the current user. Again, after a user is authenticated once, the Name property continues to hold the username. Finally, the Ticket property represents the Authentication Ticket. The FormsAuthenticationTicket class has the following properties:
The ASP.NET page in Listing 19.9 demonstrates how you can retrieve and display all these properties within an ASP.NET page. (This page shows something interesting only if the user has already been authenticated before requesting the page, see Figure 19.2.) Listing 19.9 FormsIdentity\FormsIdentity.aspx<%@ debug="True" %> <Script Runat="Server"> Sub Page_Load Dim objUserIdentity As FormsIdentity Dim objTicket As FormsAuthenticationTicket If User.Identity.IsAuthenticated Then objUserIdentity = User.Identity objTicket = objUserIdentity.Ticket lblName.Text = objUserIdentity.Name lblExpiration.Text = objTicket.Expiration lblExpired.Text = objTicket.Expired lblIsPersistent.Text = objTicket.IsPersistent lblIssueDate.Text = objTicket.IssueDate lblUserData.Text = objTicket.UserData lblVersion.Text = objTicket.Version Else lblName.Text = "Who Are You?" End If End Sub </Script> <html> <head><title>FormsIdentity.aspx</title> <body> <asp:Label ID="lblName" Font-Size="18pt" Font-Bold="True" Runat="Server" /> <p> Expiration: <asp:Label ID="lblExpiration" Runat="Server" /> <p> Expired: <asp:Label ID="lblExpired" Runat="Server" /> <p> IsPersistent: <asp:Label ID="lblIsPersistent" Runat="Server" /> <p> IssueDate: <asp:Label ID="lblIssueDate" Runat="Server" /> <p> UserData: <asp:Label ID="lblUserData" Runat="Server" /> <p> Version: <asp:Label ID="lblVersion" Runat="Server" /> </body> </html> The C# version of this code can be found on the CD-ROM. Figure 19.2. Displaying user information.
Creating a Sign-Out PageIf you want to create a page that enables a user to sign out and return to anonymity, you can use the SignOut method of the FormsAuthentication class. Calling the SignOut method removes either a session or persistent cookie. A simple Sign-Out page is contained in Listing 19.10. Listing 19.10 SignOut.aspx<Script Runat="Server"> Sub Page_Load FormsAuthentication.SignOut() End Sub </Script> <html> <head><title>SignOut.aspx</title></head> <body> <h2>Goodbye!</h2> </body> </html> The C# version of this code can be found on the CD-ROM. Authenticating Users with the Web.Config FileThe Forms authentication classes are flexible enough to enable you to store usernames and passwords in many different storage mechanisms. If you need to store a small number of usernames and passwords, you can store them directly in the Web.Config file. NOTE All the files discussed in this section are located in the FormConfig directory on the CD that accompanies this book. To use these files, copy the FormConfig directory and all its subdirectories to your server. Next, you need to create a new virtual directory that has the FormConfig directory as its root directory. To store the usernames and passwords, you need to add a credentials element to the authentication section in the Web.Config file. The file in Listing 19.11 illustrates how to add two username and password entries. (This page is included under the FormConfig directory on the CD-ROM.) Listing 19.11 FormConfig\Web.Config<configuration> <system.web> <authentication mode="Forms"> <forms> <credentials passwordFormat="Clear" > <user name="Sam" password="Secret"/> <user name="Fred" password="Secret"/> </credentials> </forms> </authentication> <authorization> <deny users="?" /> </authorization> </system.web> </configuration> The C# version of this code can be found on the CD-ROM. The Web.Config file in Listing 19.11 contains both authentication and authorization sections. The authentication section contains a credentials section that contains two usernames and passwords. The authorization section prevents unauthenticated users from accessing any of the ASP.NET pages in the current directory. NOTE You must place the Web.Config file contained in Listing 19.11 in the root directory of your application. To check whether a username and password combination exists in the Web.Config file, you can use the Authenticate method of the FormsAuthentication class. This method returns the value True if the combination exists, and False otherwise . The Login.aspx page contained in Listing 19.12, for example, uses the Authenticate method to validate the username and password entered into a login form. Listing 19.12 FormConfig\Login.aspx<Script Runat="Server"> Sub Button_Click( s As Object, e As EventArgs ) If IsValid Then If FormsAuthentication.Authenticate( txtUsername.Text, txtPassword.Text ) Then FormsAuthentication.RedirectFromLoginPage( txtUsername.Text, False ) Else lblMessage.Text = "Bad username/password!" End If End If End Sub </Script> <html> <head><title>Login.aspx</title></head> <body> <form Runat="Server"> <h2>Please Login:</h2> <asp:Label ID="lblMessage" ForeColor="Red" Font-Bold="True" Runat="Server" /> <p> <b>Username:</b> <br> <asp:TextBox ID="txtUsername" Runat="Server" /> <asp:RequiredFieldValidator ControlToValidate="txtUsername" Text="Required!" Runat="Server" /> <p> <b>Password:</b> <br> <asp:TextBox ID="txtPassword" Runat="Server" /> <asp:RequiredFieldValidator ControlToValidate="txtPassword" Text="Required!" Runat="Server" /> <p> <asp:Button Text="Login!" OnClick="Button_Click" Runat="Server" /> </form> </body> </html> The C# version of this code can be found on the CD-ROM. In Listing 19.12, the Authenticate method is used in the Button_Click subroutine. If you neglect to check the Authenticate method before calling the RedirectFromLoginPage method, every user is authenticated regardless of the credential information in the Web.Config file. Encrypting Passwords in the Web.Config FileIn general, storing user passwords in clear text in the Web.Config file is not a wise idea. Any person who happens to be working on the Web server can open the Web.Config file and read everyone's secret passwords. Instead of storing the passwords in clear text, you can encrypt the passwords by using either the SHA1 or MD5 hash algorithms. For example, storing the user credentials like this can be problematic : <credentials passwordFormat="Clear" > <user name="Sam" password="Secret"/> <user name="Fred" password="Secret"/> </credentials> Instead, you can store the credentials like this: <credentials passwordFormat="SHA1" > <user name="Sam" password="9AA4F59F833D419EF8FF2B44567645A282BCD6F0"/> <user name="Fred" password="9AA4F59F833D419EF8FF2B44567645A282BCD6F0"/> </credentials> In this example, you set the value of passwordFormat to SHA1 and enter the SHA1 hash values of the passwords. Don't worry, you don't need to read a book on cryptography to compute the proper hash value for a password. The FormsAuthentication class includes the method (with the very long name) HashPasswordForStoringInConfigFile , which accepts two parameters: a plain text password to encode and the name of a hash algorithm ( SHA1 or MD5 ). The method returns the hashed password. The page in Listing 19.13 uses the HashPasswordForStoringInConfigFile method to compute the hash value for any clear text password. Listing 19.13 ComputeHash.aspx [View full width] <Script Runat="Server"> Sub Button_Click( s As Object, e As EventArgs ) Dim strHashValue As String strHashValue = FormsAuthentication.HashPasswordForStoringInConfigFile ( txtPassword.Text , lstAlgorithm.SelectedItem.Text ) lblHash.Text = strHashValue End Sub </Script> <html> <head><title>ComputeHash.Aspx</title></head> <body> <form Runat="Server"> <h2>Compute Hash Value:</h2> <b>Password:</b> <br> <asp:TextBox ID="txtPassword" Runat="Server" /> <p> <b>Algorithm:</b> <br> <asp:ListBox ID="lstAlgorithm" Runat="Server"> <asp:ListItem Text="SHA1" /> <asp:ListItem Text="MD5" /> </asp:ListBox> <p> <asp:Button Text="Compute!" OnClick="Button_Click" Runat="Server" /> <hr> Hash Value: <br> <asp:Label ID="lblHash" Runat="Server" /> </form> </body> </html> The C# version of this code can be found on the CD-ROM. Authenticating Users with an XML FileInstead of using the Web.Config file to store usernames and passwords, you can store the usernames and passwords in an XML file. In this section, you learn how to store credentials in an XML file named Passwords.xml . NOTE All the files discussed in this section are located in the FormsXml directory on the CD that accompanies this book. To use these files, copy the FormsXml directory and all its subdirectories to your server. Next, create a new virtual directory that has the FormsXml directory as its root directory. For this example, you need to create four files:
The first file should be familiar; it's the standard Web.Config file for Forms authentication. This file is included in Listing 19.14. Listing 19.14 FormsXml\Web.Config<configuration> <system.web> <authentication mode="Forms" /> <authorization> <deny users="?" /> </authorization> </system.web> </configuration> The C# version of this code can be found on the CD-ROM. Next, you need to create the Passwords.xml file in which you can store all the usernames and passwords. The Passwords.xml file is contained in Listing 19.15. Listing 19.15 FormsXml\Passwords.xml<Passwords> <user> <name>Sam</name> <password>Secret</password> </user> <user> <name>Fred</name> <password>Secret</password> </user> </Passwords> The C# version of this code can be found on the CD-ROM. When you validate a username and password, the username and password combination is checked against the Passwords.xml file. When a new user registers, the new user's username and password are added to this file. The Login.aspx page is contained in Listing 19.16. Listing 19.16 FormsXml\Login.aspx<%@ Import Namespace="System.Data" %> <Script Runat="Server"> Sub Page_Load Dim strLinkPath As String If Not IsPostBack Then strLinkPath = String.Format( "Register/Register.aspx?ReturnUrl={0}", _ Request.Params( "ReturnUrl" ) ) lnkRegister.NavigateUrl = String.Format( strLinkPath ) End If End Sub Sub Button_Click( s As Object, e As EventArgs ) If IsValid Then If XmlAuthenticate( txtUsername.Text, txtPassword.Text ) Then FormsAuthentication.RedirectFromLoginPage( txtUsername.Text, False ) End If End If End Sub Function XmlAuthenticate( strUsername As String, strPassword As String ) As Boolean Dim dstPasswords As DataSet Dim dtblPasswords As DataTable Dim arrUsers() As DataRow dstPasswords = New DataSet() dstPasswords.ReadXml( MapPath( "Passwords.xml" ) ) dtblPasswords = dstPasswords.Tables( 0 ) arrUsers = dtblPasswords.Select( "name='" & strUsername & "'" ) If arrUsers.Length > 0 Then If arrUsers( 0 )( "password" ) = strPassword Then Return True Else lblMessage.Text = "Incorrect Password!" End If Else lblMessage.Text = "Username Not Found!" End If Return False End Function </Script> <html> <head><title>Login.aspx</title></head> <body> <form Runat="Server"> <h2>Please Login:</h2> <asp:Label ID="lblMessage" ForeColor="Red" Font-Bold="True" Runat="Server" /> <p> <b>Username:</b> <br> <asp:TextBox ID="txtUsername" Runat="Server" /> <asp:RequiredFieldValidator ControlToValidate="txtUsername" Text="Required!" Runat="Server" /> <p> <b>Password:</b> <br> <asp:TextBox ID="txtPassword" Runat="Server" /> <asp:RequiredFieldValidator ControlToValidate="txtPassword" Text="Required!" Runat="Server" /> <p> <asp:Button Text="Login!" OnClick="Button_Click" Runat="Server" /> <hr> <asp:HyperLink ID="lnkRegister" Text="Click Here To Register!" Runat="Server" /> </form> </body> </html> The C# version of this code can be found on the CD-ROM. The page in Listing 19.16 contains a function named XmlAuthenticate . This function returns the value True if the username and password entered into the HTML form exist in the Passwords.xml file; otherwise, the function returns the value False . The function loads the Passwords.xml file into a DataSet with the ReadXml method. Next, it uses the DataSet 's Select method to retrieve any entries in the Passwords.xml file in which the name element matches the name entered into the form. If the username and password exist, the function returns the value True . If XmlAuthenticate returns the value True , the RedirectFromLoginPage method is called. This method issues a new cookie containing an Authentication Ticket and automatically redirects the user's browser back to the page that was originally requested . Notice that the Login.aspx page includes a link to the Register.aspx page. The user can click this link to register a new username and password for the Web site. A query string variable named ReturnUrl is added to the link to Register.aspx in the Page_Load subroutine. This query string variable contains the path to the original page that the user requested before being redirected to the Login.aspx page. The Register.aspx page is contained in a separate directory from the Login.aspx page. Storing pages separately is necessary because all the pages, except the Login.aspx page, require authentication to access. The Register.aspx page, shown in Listing 19.17, is contained in a directory that includes a Web.Config file that enables access for all users. Listing 19.17 FormsXml\Register\Register.aspx<%@ Import Namespace="System.Data" %> <Script Runat="Server"> Sub Button_Click( s As Object, e As EventArgs ) Dim dstPasswords As DataSet Dim dtblPasswords As DataTable Dim arrUsers() As DataRow Dim drowNew As DataRow If IsValid Then dstPasswords = New DataSet() dstPasswords.ReadXml( MapPath( "../Passwords.xml" ) ) dtblPasswords = dstPasswords.Tables( 0 ) arrUsers = dtblPasswords.Select( "Name='" & txtUsername.Text & "'" ) If arrUsers.Length > 0 Then lblMessage.Text = "Username Already Registered!" Else drowNew = dtblPasswords.NewRow() drowNew( "Name" ) = txtUsername.Text drowNew( "Password" ) = txtPassword.Text dtblPasswords.Rows.Add( drowNew ) dstPasswords.WriteXml( MapPath( "../Passwords.xml" ) ) FormsAuthentication.RedirectFromLoginPage( txtUsername.Text, False ) End If End If End Sub </Script> <html> <head><title>Register.aspx</title></head> <body> <form Runat="Server"> <h2>Please Register</h2> <asp:Label ID="lblMessage" ForeColor="Red" Font-Bold="True" EnableViewState="False" Runat="Server" /> <p> <b>Username:</b> <br> <asp:TextBox ID="txtUsername" Runat="Server" /> <asp:RequiredFieldValidator ControlToValidate="txtUsername" Text="Required!" Runat="Server" /> <p> <b>Password:</b> <br> <asp:TextBox ID="txtPassword" Runat="Server" /> <asp:RequiredFieldValidator ControlToValidate="txtPassword" Text="Required!" Runat="Server" /> <p> <asp:Button Text="Register!" OnClick="Button_Click" Runat="Server" /> </form> </body> </html> The C# version of this code can be found on the CD-ROM. The Register.aspx page, like the Login.aspx page, contains a form with fields for a username and password. When the form is submitted, the Button_Click subroutine is executed. Before doing anything else, the Button_Click subroutine checks whether the username entered into the form already exists in the Passwords.xml file. You don't want the same username registered by more than one user. If the username has already been registered, an error is displayed. If the username has not been registered, the new username and password are added to the Passwords.xml file. Next, the RedirectFromLoginPage method is called to send the user back to the original page he or she requested. Authenticating Users with a Database TableThe most common method of creating a custom user registration system is to use a database table to hold usernames and passwords. For large Web sites, this solution is the best because it is the most scalable. NOTE All the files discussed in this section are located in the FormsDB directory on the CD that accompanies this book. To use these files, you need to copy the FormsDB directory and all its subdirectories to your server. Next, create each of the database objects. Finally, you need to create a new virtual directory that points to FormsDB . In this section, you are going to create a user registration system with Forms authentication that works with Microsoft SQL Server. To create this system, you need to create the following objects:
You use SQL Server stored procedures to validate usernames and passwords and to register new users for performance reasons. The first file that you need to create is Web.Config . This file, included in Listing 19.18, should be familiar to you because it contains the standard sections to enable Forms authentication. Listing 19.18 FormsDB\Web.Config<configuration> <system.web> <authentication mode="Forms" /> <authorization> <deny users="?" /> </authorization> </system.web> </configuration> The C# version of this code can be found on the CD-ROM. You store usernames and passwords in a SQL Server database table. You can create this table by using either the SQL Enterprise Manager or SQL Query Analyzer. The SQL command used to create the UserList table is contained in Listing 19.19. Listing 19.19 FormsDB\UserList.sqlCREATE TABLE UserList ( u_id INT NOT NULL IDENTITY, u_username VARCHAR( 100 ), u_password VARCHAR( 100 ) ) The C# version of this code can be found on the CD-ROM. Next, you need to create the Login.aspx page. This page, contained in Listing 19.20, has a form that enables users to log in with their usernames and passwords. Listing 19.20 FormsDB\Login.aspx<%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.SqlClient" %> <Script Runat="Server"> Sub Page_Load Dim strLinkPath As String If Not IsPostBack Then strLinkPath = String.Format( "Register/Register.aspx?ReturnUrl={0}", _ Request.Params( "ReturnUrl" ) ) lnkRegister.NavigateUrl = String.Format( strLinkPath ) End If End Sub Sub Button_Click( s As Object, e As EventArgs ) If IsValid Then If DBAuthenticate( txtUsername.Text, txtPassword.Text ) > 0 Then FormsAuthentication.RedirectFromLoginPage( txtUsername.Text, False ) End If End If End Sub Function DBAuthenticate( strUsername As String, strPassword As String ) As Integer Dim conMyData As SqlConnection Dim cmdSelect As SqlCommand Dim parmReturnValue As SqlParameter Dim intResult As Integer conMyData = New SqlConnection( "Server=localhost;UID=sa;PWD=secret; Database=myData" ) cmdSelect = New SqlCommand( "DBAuthenticate", conMyData ) cmdSelect.CommandType = CommandType.StoredProcedure parmReturnValue = cmdSelect.Parameters.Add( "RETURN_VALUE", SqlDbType.Int ) parmReturnValue.Direction = ParameterDirection.ReturnValue cmdSelect.Parameters.Add( "@username", strUsername ) cmdSelect.Parameters.Add( "@password", strPassword ) conMyData.Open() cmdSelect.ExecuteNonQuery() intResult = cmdSelect.Parameters( "RETURN_VALUE" ).Value conMyData.Close() If intResult < 0 Then If intResult = -1 Then lblMessage.Text = "Username Not Registered!" Else lblMessage.Text = "Invalid Password!" End If End If Return intResult End Function </Script> <html> <head><title>Login.aspx</title></head> <body> <form Runat="Server"> <h2>Please Login:</h2> <asp:Label ID="lblMessage" ForeColor="Red" Font-Bold="True" Runat="Server" /> <p> <b>Username:</b> <br> <asp:TextBox ID="txtUsername" Runat="Server" /> <asp:RequiredFieldValidator ControlToValidate="txtUsername" Text="Required!" Runat="Server" /> <p> <b>Password:</b> <br> <asp:TextBox ID="txtPassword" Runat="Server" /> <asp:RequiredFieldValidator ControlToValidate="txtPassword" Text="Required!" Runat="Server" /> <p> <asp:Button Text="Login!" OnClick="Button_Click" Runat="Server" /> <hr> <asp:HyperLink ID="lnkRegister" Text="Click Here To Register!" Runat="Server" /> </form> </body> </html> The C# version of this code can be found on the CD-ROM. The Login.aspx page contains a function named DBAuthenticate . This function matches the username and password entered into the login form against the UserList database table with the DBAuthenticate stored procedure. If the user can be authenticated, the RedirectFromLoginPage method is called. This method issues a new Authentication Ticket cookie and redirects the user back to the original page he or she requested. If the user cannot be found, an error message is displayed. The Login.aspx page uses a stored procedure named DBAuthenticate . This stored procedure is contained in Listing 19.21. Listing 19.21 FormsDB\DBAuthenticate.sqlCREATE PROCEDURE DBAuthenticate ( @username Varchar( 100 ), @password Varchar( 100 ) ) As DECLARE @ID INT DECLARE @actualPassword Varchar( 100 ) SELECT @ID = IdentityCol, @actualPassword = u_password FROM UserList WHERE u_username = @username IF @ID IS NOT NULL IF @password = @actualPassword RETURN @ID ELSE RETURN - 2 ELSE RETURN - 1 The C# version of this code can be found on the CD-ROM. The DBAuthenticate stored procedure returns one of three values. If the username and password passed to the procedure exist in the UserList table, the stored procedure returns the ID of the user. If the username cannot be found, the procedure returns the value -1 . If the username is found but the password is invalid, the procedure returns -2 . The Login.aspx page contains a link to the Register.aspx page, which enables new users to register at your Web site. The code for the Register.aspx page is contained in Listing 19.22. NOTE The Register.aspx page is contained in its own directory with its own Web.Config file. The Web.Config file enables anonymous access to files in the directory. Enabling access this way is necessary to enable unauthenticated users to register. (You could also enable anonymous access to the Register.aspx page with the location element in the root Web.Config file.) Listing 19.22 FormsDB\Register\Register.aspx<%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.SqlClient" %> <Script Runat="Server"> Sub Button_Click( s As Object, e As EventArgs ) Dim conMyData As SqlConnection Dim cmdSelect As SqlCommand Dim parmReturnValue As SqlParameter Dim intResult As Integer If IsValid Then conMyData = New SqlConnection( "Server=localhost;UID=sa;PWD=secret; Database=myData" ) cmdSelect = New SqlCommand( "DBRegister", conMyData ) cmdSelect.CommandType = CommandType.StoredProcedure parmReturnValue = cmdSelect.Parameters.Add( "RETURN_VALUE", SqlDbType.Int ) parmReturnValue.Direction = ParameterDirection.ReturnValue cmdSelect.Parameters.Add( "@username", txtUsername.Text ) cmdSelect.Parameters.Add( "@password", txtPassword.Text ) conMyData.Open() cmdSelect.ExecuteNonQuery() intResult = cmdSelect.Parameters( "RETURN_VALUE" ).Value conMyData.Close() If intResult = - 1 Then lblMessage.Text = "Username Already Registered!" Else FormsAuthentication.RedirectFromLoginPage( txtUsername.Text, False ) End If End If End Sub </Script> <html> <head><title>Register.aspx</title></head> <body> <form Runat="Server"> <h2>Please Register</h2> <asp:Label ID="lblMessage" ForeColor="Red" Font-Bold="True" EnableViewState="False" Runat="Server" /> <p> <b>Username:</b> <br> <asp:TextBox ID="txtUsername" Runat="Server" /> <asp:RequiredFieldValidator ControlToValidate="txtUsername" Text="Required!" Runat="Server" /> <p> <b>Password:</b> <br> <asp:TextBox ID="txtPassword" Runat="Server" /> <asp:RequiredFieldValidator ControlToValidate="txtPassword" Text="Required!" Runat="Server" /> <p> <asp:Button Text="Register!" OnClick="Button_Click" Runat="Server" /> </form> </body> </html> The C# version of this code can be found on the CD-ROM. The Register.aspx page contained in Listing 19.22 has one main subroutine named Button_Click . This subroutine adds a new username and password to the UserList table if the username does not already exist. The Button_Click subroutine uses a SQL stored procedure named DBRegister , contained in Listing 19.23, to add the new user information. Listing 19.23 FormsDB\DBRegister.sqlCREATE PROCEDURE DBRegister ( @username Varchar( 100 ), @password Varchar( 100 ) ) AS IF EXISTS( SELECT u_id FROM UserList WHERE u_username=@username ) RETURN - 1 ELSE INSERT UserList ( u_username, u_password ) VALUES ( @username, @password ) RETURN @@IDENTITY The C# version of this code can be found on the CD-ROM. The DBRegister stored procedure contained in Listing 19.23 returns one of two values. If the username passed to the stored procedure already exists in the UserList table, the procedure returns the value -1 . Otherwise, the stored procedure adds the new username and password and returns the ID for the user. Implementing Roles-based AuthenticationIn certain situations, it makes sense to divide the users of your Web site into different roles. Dividing users into roles is useful when you want to grant or deny different permissions to different groups of users. For example, you might want to permit users in the supervisors role to edit certain data that users in the sales role cannot. NOTE All the files discussed in this section are located in the FormsRoles directory on the CD that accompanies this book. To use these files, you need to copy the FormsRoles directory to your server. Next, create a new virtual directory that points to the FormsRoles directory. You can assign authenticated users to different roles within the Application_AuthenticateRequest subroutine in the Global.asax file. To assign a set of roles to a user, you use statements like the following: Dim arrRoles As String() = { "Supervisors", "Engineers" } Context.User = New GenericPrincipal( Context.User.Identity, arrRoles ) The first statement creates a new string array of role names. The second statement associates the current authenticated user with the list of roles. Imagine that you have created an XML file that contains a list of roles for certain users of your Web site. This XML file is contained in Listing 19.24. Listing 19.24 FormsRoles\Roles.xml<roles> <user name="Bob" roles="Sales" /> <user name="Jane" roles="Supervisor,Sales" /> </roles> The C# version of this code can be found on the CD-ROM. The XML file in Listing 19.24 associates Bob with the Sales role and Jane with the Supervisor role and Sales roles. After you create the Roles.xml file, you can use the file to assign users to different roles with the Global.asax file in Listing 19.25. Listing 19.25 FormsRoles\Global.asax<%@ Import Namespace="System.Security.Principal" %> <%@ Import Namespace="System.Xml" %> <Script Runat="Server"> Sub Application_AuthenticateRequest( s As Object, e As EventArgs ) Dim strUserName As String Dim objRoles As XmlDocument Dim arrRoles As String() Dim objNode As XmlNode Dim strXPath As String objRoles = GetRoles() If Context.Request.IsAuthenticated Then strUserName = Context.User.Identity.Name strXPath = String.Format( "user[@name='{0}']", strUserName ) objNode = objRoles.DocumentElement.SelectSingleNode( strXPath ) If Not IsNothing( objNode ) Then arrRoles = objNode.Attributes( "roles" ).Value.Split Context.User = New GenericPrincipal( Context.User.Identity, arrRoles ) End If End If End Sub Function GetRoles() As XmlDocument Dim objRoles As XmlDocument objRoles = Context.Cache( "Roles" ) If objRoles Is Nothing Then objRoles = New XmlDocument objRoles.Load( Server.MapPath( "Roles.xml" ) ) Context.Cache.Insert( _ "Roles", _ objRoles, _ New CacheDependency( Server.MapPath( "Roles.xml" ) ) ) End If Return objRoles End Function </Script> The C# version of this code can be found on the CD-ROM. The Global.asax file in Listing 19.25 contains one function and one subroutine. The function, named GetRoles() , retrieves an XmlDocument that represents the Roles.xml file. This file is either retrieved from the cache or from the file system. The Application_AuthenticateRequest subroutine is fired whenever a user requests a page. This subroutine retrieves the list of roles associated with the current users from the list of roles returned by the GetRoles() function. Next, the subroutine assigns the list of roles to the current user. You can test the files in Listings 19.24 and 19.25 with the page in Listing 19.26. Listing 19.26 FormsRoles\Default.aspx<html> <head><title>Default.aspx</title></head> <body> <h1>Welcome <%=User.Identity.Name%></h1> <% If User.IsInRole( "Supervisor" ) Then Response.Write( "You have supervisor permissions!" ) Else Response.Write( "You don't have supervisor permissions!" ) End If %> </body> </html> The C# version of this code can be found on the CD-ROM. The page in Listing 19.26 uses the IsInRole method to display different messages depending on whether the current user is a member of the Supervisor role or not. Creating a Custom Authentication TicketForms authentication uses an Authentication Ticket stored in a cookie to authenticate users each time they request a page. The Authentication Ticket is automatically generated and added to a user's browser by the RedirectFromLoginPage method. In most cases, the Authentication Ticket automatically generated by the RedirectFromLoginPage method contains all the information you need. You can use this ticket to retrieve such information as the username and date the cookie was issued. However, you also have the option of creating the Authentication Ticket yourself. Why would you want to create the Authentication Ticket manually? If you create it yourself, you can add additional information to it by using the UserData property. For example, you can use the UserData property to store a unique ID or the email address for each user. After the information is stored in the Authentication Ticket, you can retrieve this information from the ticket on each page the user requests. CAUTION An Authentication Ticket is stored in a browser cookie, and browser cookies have size limitations. For example, the original Netscape cookie specification places a 4KB limit on the size of any cookie (the size refers to the combined size of the cookie's name and value). The Authentication Ticket is represented by the FormsAuthenticationTicket class. If you want to create an Authentication Ticket that contains custom data, you need to explicitly create an object of this class and add it to the browser's cookie collection. You can explicitly create a new Authentication Ticket by passing the following parameters to the FormsAuthenticationTicket constructor:
To illustrate the process of creating a custom Authentication Ticket, you are going to create three files:
NOTE All the files discussed in this section are located in the AuthenticationTicket directory on the CD that accompanies this book. To use these files, copy the AuthenticationTicket directory to your server and create a new virtual directory that points to it. The standard Web.Config file for this example is contained in Listing 19.27. Listing 19.27 AuthenticationTicket\Web.Config<configuration> <system.web> <authentication mode="Forms" /> <authorization> <deny users="?" /> </authorization> </system.web> </configuration> The C# version of this code can be found on the CD-ROM. If an unauthenticated user attempts to access a page in a directory that contains the Web.Config file in Listing 19.27, the user is automatically redirected to the Login.aspx page. This page is contained in Listing 19.28. Listing 19.28 AuthenticationTicket\Login.aspx<Script Runat="Server"> Sub Button_Click( s As Object, e As EventArgs ) Dim objTicket As FormsAuthenticationTicket Dim objCookie As HttpCookie Dim strReturnURL As String objTicket = New FormsAuthenticationTicket( _ 1, _ txtUsername.Text, _ NOW(), _ #12/25/2002#, _ False, _ txtUserdata.Text ) objCookie = New HTTPCookie( ".ASPXAUTH" ) objCookie.Value = FormsAuthentication.Encrypt( objTicket ) Response.Cookies.Add( objCookie ) strReturnURL = Request.Params( "ReturnURL" ) If strReturnURL <> Nothing Then Response.Redirect( strReturnURL ) Else Response.Redirect( "Default.aspx" ) End If End Sub </Script> <html> <head><title>Login.aspx</title></head> <body> <form Runat="Server"> <b>Username:</b> <br> <asp:TextBox ID="txtUsername" Runat="Server" /> <p> <b>User Data:</b> <br> <asp:TextBox ID="txtUserdata" Runat="Server" /> <p> <asp:button Text="Add Ticket!" OnClick="Button_Click" Runat="Server" /> </form> </body> </html> The C# version of this code can be found on the CD-ROM. The Login.aspx page in Listing 19.28 contains a form for entering a username and custom user data. When the form is submitted, the Button_Click subroutine is executed. The Button_Click subroutine creates a new Authentication Ticket named objTicket . The username and userdata form fields supply the values of the name and userData properties of the new ticket. Next, the custom Authentication Ticket is added to the browser's cookie collection with the Add method of the HttpCookiesCollection object. Finally, the user is redirected back to the page indicated by the ReturnURL query string variable. If this query string variable doesn't exist, the user is redirected to the Default.aspx page. To test whether the custom user data was actually added to the Authentication Ticket, you can use the Default.aspx page contained in Listing 19.29. Listing 19.29 AuthenticationTicket\Default.aspx<Script Runat="Server"> Sub Page_Load Dim objFormsID As FormsIdentity objFormsID = User.Identity lblName.Text = objFormsID.Ticket.Name lblUserData.Text = objFormsID.Ticket.UserData End Sub </Script> <html> <head><title>Default.aspx</title></head> <body> <b>Name:</b> <asp:Label ID="lblName" Runat="Server" /> <p> <b>User Data:</b> <asp:Label ID="lblUserData" Runat="Server" /> </body> </html> The C# version of this code can be found on the CD-ROM. In the Page_Load subroutine in Listing 19.29, the identity of the current user is retrieved from the User property of the Page class. The identity of the current user is represented by an instance of the FormsIdentity class, which includes the Authentication Ticket. Both the name and custom user data associated with the current user are retrieved from the instance of the FormsIdentity class. Both bits of information are displayed in Label controls. Using Forms Authentication and Web FarmsBy default, you cannot share the same Authentication Ticket cookie across multiple servers. This means that, without some extra work, you cannot use Forms Authentication with servers clustered into a Web farm. If you want to use Forms Authentication across multiple servers, you need to configure all the servers to share the same validation and decryption keys. By default, each server automatically generates its own keys. To force multiple servers to use common keys, modify the following machineKey section in either an application root Web.Config file or the Machine.Config file: <machineKey validationKey="AutoGenerate, IsolateApps" decryptionKey="AutoGenerate, IsolateApps" validation="SHA1"/> You need to replace autogenerate with a valid key. The key must be between 40 and 128 characters (the recommendation is 128 characters). You can pick any random string of characters for the validationKey and decryptionKey values. NOTE The IsolateApps modifier was introduced with version 1.1 of the ASP.NET Framework. It causes different keys to be generated for different applications running on the same server. If you need to share the same keys across multiple ASP.NET applications, then you need to remove the IsolateApps modifier. |