Pulling It All Together

for RuBoard

So far we have looked at each feature in isolation. Let's try to pull together a realistic example that you might be able to use in your work that combines all these concepts. You are going to create a Web site, as mentioned earlier, that contains three authenticated and authorized subdirectories: attendees, publish, and admin. Forms authentication will be used to authenticate the users against a Microsoft SQL Server “based credential store. URL authorization will be used to protect the subdirectories based on role information stored in Microsoft SQL Server. First, you need to create a web.config file that turns on forms authentication and defines the authorization elements for the appropriate subdirectories. Listing 7.15 shows the web.config.

Listing 7.15 Web.config File Sets Authentication to Forms and Defines the URL Authorization Settings for the Three Subdirectories
 <?xml version="1.0" encoding="utf-8" ?> <configuration>     <system.web>         <authentication mode="Forms">             <forms loginUrl="login.aspx" />         </authentication>         <authorization>             <allow users="*" /> <!-- Allow all users -->         </authorization>     </system.web>     <location path="admin">         <system.web>             <authorization>                 <allow roles="Administrator" />                 <deny users="*" />             </authorization>         </system.web>     </location>     <location path="publish">         <system.web>             <authorization>                 <allow roles="Administrator,Publisher" />                 <deny users="*" />             </authorization>         </system.web>     </location>     <location path="attendee">         <system.web>             <authorization>                 <allow roles="Administrator,Publisher,Attendee" />                 <deny users="*" />             </authorization>         </system.web>     </location> </configuration> 

This sets up the following restrictions:

  • The admin directory requires the Administrator role

  • The publish directory accepts either the Administrator or Publisher roles

  • The attendee directory accepts the Administrator, Publisher, or Attendee roles

After this structure is in place, you need to create a login page as in the previous examples. The HTML for this login page is similar to the ones we have shown before; however, the code behind it is very different.

In this example, you are storing the roles associated with a user in Microsoft SQL Server. Each time the user comes back to the site after the initial authentication, you need to add the role information to the Principal as shown in earlier examples. Hitting the database on every request just to retrieve the role information is clearly inefficient. You could potentially cache the role information in Session() , but if you are operating in a Web farm, you would have to make sure you are using some form of shared Session state. Remember, however, that each time you authenticate a user, a cookie is sent down and used for future authentications. It appears to be an ideal location to store the role information. As it turns out, the ticket that is stored in the cookie is represented by the FormsAuthenticationTicket class.

FormsAuthenticationTicket

Member of System.Web.Security.

Assembly: System.Web.dll.

The FormsAuthenticationTicket class represents the data that is encrypted and stored in a cookie for use in forms authentication.

Properties
CookiePath Expiration Expired
IsPersistent IssueDate Name
UserData Version  

This class provides a member, UserData , that can be used to store the role information. This member is a string, not a name/value collection as you might expect. During the initial request on retrieving the role information from the database, you will place it into a comma-separated value string and place this string into the UserData member.

NOTE

Remember that the UserData is passed back and forth from the client to the server on potentially every request. You don't want to store a large amount of data in UserData , because it will slow down performance.


During future requests , you will retrieve the role information from the UserData and use the Split() function to break it up into a string array suitable for passing to the GenericPrincipal constructor. One downside of doing this is that you can no longer use the simple RedirectFromLoginPage() function in the Login page. It instead must do all the work to create the ticket, encrypt it, add it to the Response.Cookies collection, and finally redirect the user to the initial page that he requested . Listings 7.16 and 7.17 show login.aspx, which implements all this functionality.

Listing 7.16 The HTML for login.aspx
 <%@ Page language="c#" Codebehind="login.aspx.cs" AutoEventWireup="false" Inherits="DBFormURL.login" %> <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" > <HTML>     <HEAD>         <meta name="GENERATOR" Content="Microsoft Visual Studio 7.0">         <meta name="CODE_LANGUAGE" Content="C#">         <meta name="vs_defaultClientScript" content="JavaScript (ECMAScript)">         <meta name="vs_targetSchema" content="http://schemas.microsoft.com/ intellisense/ie5">     </HEAD>     <body MS_POSITIONING="GridLayout">         <form id="login" method="post" runat="server">             <asp:Label id="lblEmail" style="Z-INDEX: 101; LEFT: 8px; POSITION: absolute; TOP: 8px" runat="server">Email:</asp:Label>             <asp:TextBox id="txtEmail" style="Z-INDEX: 102; LEFT: 78px; POSITION: absolute; TOP: 5px" runat="server"></asp:TextBox>             <asp:Label id="lblPassword" style="Z-INDEX: 103; LEFT: 8px; POSITION: absolute; TOP: 44px" runat="server">Password:</asp:Label>             <asp:TextBox id="txtPassword" style="Z-INDEX: 104; LEFT: 78px; POSITION: absolute; TOP: 39px" runat="server" TextMode="Password"></asp:TextBox>             <asp:Button id="btnLogin" style="Z-INDEX: 105; LEFT: 249px; POSITION: absolute; TOP: 6px" runat="server" Text="Login"></asp:Button>             <asp:RequiredFieldValidator id="rfvEmail" style="Z-INDEX: 106; LEFT: 13px; POSITION: absolute; TOP: 78px" runat="server" ErrorMessage="You must enter an email address." ControlToValidate="txtEmail"></asp:RequiredFieldValidator>             <asp:RequiredFieldValidator id="rfvPassword" style="Z-INDEX: 107; LEFT: 13px; POSITION: absolute; TOP: 105px" runat="server" ErrorMessage="You must enter a password." ControlToValidate="txtPassword"></asp:RequiredFieldValidator>             <asp:Label id="lblInvalidPassword" style="Z-INDEX: 108; LEFT: 13px; POSITION: absolute; TOP: 135px" runat="server" ForeColor="Red" Visible="False">Invalid password.</asp:Label>         </form>     </body> </HTML> 
Listing 7.17 The Class for the login.aspx Page in Listing 7.16
 using System; using System.Collections; using System.ComponentModel; using System.Data; using System.Data.SqlClient; using System.Drawing; using System.Web; using System.Web.SessionState; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; namespace DBFormURL {     /// <summary>     /// Summary description for login.     /// </summary>     public class login : System.Web.UI.Page     {         protected System.Web.UI.WebControls.Label lblEmail;         protected System.Web.UI.WebControls.TextBox txtEmail;         protected System.Web.UI.WebControls.Label lblPassword;         protected System.Web.UI.WebControls.TextBox txtPassword;         protected System.Web.UI.WebControls.Button btnLogin;         protected System.Web.UI.WebControls.RequiredFieldValidator rfvEmail;         protected System.Web.UI.WebControls.RequiredFieldValidator rfvPassword;         protected System.Web.UI.WebControls.Label lblInvalidPassword;         public login()         {             Page.Init += new System.EventHandler(Page_Init);         }         private void Page_Load(object sender, System.EventArgs e)         {             // Put user code to initialize the page here         }         private void Page_Init(object sender, EventArgs e)         {             //             // CODEGEN: This call is required by the ASP.NET Web Form Designer.             //             InitializeComponent();         }         #region Web Form Designer generated code         /// <summary>         /// Required method for Designer support - do not modify         /// the contents of this method with the code editor.         /// </summary>         private void InitializeComponent()         {             this.btnLogin.Click += new System.EventHandler(this.btnLogin_Click);             this.Load += new System.EventHandler(this.Page_Load);         }         #endregion         private void btnLogin_Click(object sender, System.EventArgs e)         {         SqlDataReader sdr;         // Create a connection         SqlConnection sc = new SqlConnection(Application["DSN"].ToString());         // Open the database connection         sc.Open();         // Create a command to get the user         SqlCommand cmd = new SqlCommand("GetUser '" + txtEmail.Text + "', '" + txtPassword.Text + "'", sc);         // Execute the command         sdr = cmd.ExecuteReader();         // Attempt to read the first record             if(sdr.Read())             {                 // close the datareader                 sdr.Close();                 // Get the list of roles the user is in                 SqlDataReader drRoles;                 SqlCommand cmdRoles = new SqlCommand("GetRoles '" + txtEmail.Text + "'", sc);                 ArrayList arRoles = new ArrayList();                 // Execute the command                 drRoles = cmdRoles.ExecuteReader();                 // Get a string builder to store the roles in a csv list                 System.Text.StringBuilder bldr = new System.Text.StringBuilder();                 // Loop through the list of roles and get them                 while(drRoles.Read())                 {                     bldr.Append(drRoles["Role"]);                     bldr.Append(",");                 }                 // Strip the last comma                 bldr.Remove(bldr.Length - 1, 1);                 // Create an authentication ticket                 // Place a serialized representation of the roles into the authentication ticket                 System.Web.Security.FormsAuthenticationTicket ticket = new System.Web.Security.FormsAuthenticationTicket(1, txtEmail.Text, DateTime.Now, DateTime.Now.AddMinutes(20), false, bldr.ToString());                 // Get the encrypted version of the ticket                 string strEncrypted = System.Web.Security.FormsAuthentication.Encrypt(ticket);                 // Put it into a cookie                 HttpCookie hc = new HttpCookie(System.Web.Security. FormsAuthentication.FormsCookieName, strEncrypted);                 hc.Expires = DateTime.Now.AddMinutes(20);                 // Add it to the cookies collection                 Response.Cookies.Add(hc);                 // Redirect the user to the page they requested                 string strReturnURL = Request.Params["ReturnUrl"].ToString();                 if(strReturnURL != "") Response.Redirect(strReturnURL);             }             else             {                 // Show a message that the credentials are invalid                 lblInvalidPassword.Visible = false;             }         }     End Sub     } } 

This code relies on three tables in Microsoft SQL Server to store the credentials: Users, Roles, and UserRoleMappings. Figure 7.2 shows the relationships between these tables. Listing 7.18 is a script that can be used to create the tables and stored procedures that are used by the login.aspx page.

Figure 7.2. The relationships between the Users, Roles, and UserRoleMappings tables.

graphics/07fig02.gif

Listing 7.18 The Transact SQL to Create the Tables and Stored Procedures Used by login.aspx
 IF EXISTS (SELECT name FROM master.dbo.sysdatabases WHERE name = N'SecuritySample')     DROP DATABASE [SecuritySample] GO CREATE DATABASE [SecuritySample]  ON (NAME = N'SecuritySample_Data', FILENAME = N'c:\Program Files\Microsoft SQL Server\MSSQL\data\SecuritySample_Data.MDF' , SIZE = 1, FILEGROWTH = 10%) LOG ON (NAME = N'SecuritySample_Log', FILENAME = N'C:\ Program Files\Microsoft SQL Server\MSSQL\data\SecuritySample_Log.LDF' , SIZE = 1, FILEGROWTH = 10%)  COLLATE SQL_Latin1_General_CP1_CI_AS GO exec sp_dboption N'SecuritySample', N'autoclose', N'false' GO exec sp_dboption N'SecuritySample', N'bulkcopy', N'false' GO exec sp_dboption N'SecuritySample', N'trunc. log', N'false' GO exec sp_dboption N'SecuritySample', N'torn page detection', N'true' GO exec sp_dboption N'SecuritySample', N'read only', N'false' GO exec sp_dboption N'SecuritySample', N'dbo use', N'false' GO exec sp_dboption N'SecuritySample', N'single', N'false' GO exec sp_dboption N'SecuritySample', N'autoshrink', N'false' GO exec sp_dboption N'SecuritySample', N'ANSI null default', N'false' GO exec sp_dboption N'SecuritySample', N'recursive triggers', N'false' GO exec sp_dboption N'SecuritySample', N'ANSI nulls', N'false' GO exec sp_dboption N'SecuritySample', N'concat null yields null', N'false' GO exec sp_dboption N'SecuritySample', N'cursor close on commit', N'false' GO exec sp_dboption N'SecuritySample', N'default to local cursor', N'false' GO exec sp_dboption N'SecuritySample', N'quoted identifier', N'false' GO exec sp_dboption N'SecuritySample', N'ANSI warnings', N'false' GO exec sp_dboption N'SecuritySample', N'auto create statistics', N'true' GO exec sp_dboption N'SecuritySample', N'auto update statistics', N'true' GO use [SecuritySample] GO if exists (select * from dbo.sysobjects where id = object_id(N'[dbo]. [FK_UserRoleMapping_Roles]') and OBJECTPROPERTY(id, N'IsForeignKey') = 1) ALTER TABLE [dbo].[UserRoleMapping] DROP CONSTRAINT FK_UserRoleMapping_Roles GO if exists (select * from dbo.sysobjects where id = object_id(N'[dbo]. [FK_UserRoleMapping_Users]') and OBJECTPROPERTY(id, N'IsForeignKey') = 1) ALTER TABLE [dbo].[UserRoleMapping] DROP CONSTRAINT FK_UserRoleMapping_Users GO if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[GetRoles]') and OBJECTPROPERTY(id, N'IsProcedure') = 1) drop procedure [dbo].[GetRoles] GO if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[GetUser]') and OBJECTPROPERTY(id, N'IsProcedure') = 1) drop procedure [dbo].[GetUser] GO if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[Roles]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [dbo].[Roles] GO if exists (select * from dbo.sysobjects where id = object_id(N'[dbo]. [UserRoleMapping]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [dbo].[UserRoleMapping] GO if exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[Users]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [dbo].[Users] GO CREATE TABLE [dbo].[Roles] (     [RoleID] [int] IDENTITY (1, 1) NOT NULL ,     [Role] [varchar] (50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ) ON [PRIMARY] GO CREATE TABLE [dbo].[UserRoleMapping] (     [MappingID] [int] IDENTITY (1, 1) NOT NULL ,     [UserID] [int] NOT NULL ,     [RoleID] [int] NOT NULL ) ON [PRIMARY] GO CREATE TABLE [dbo].[Users] (     [UserID] [int] IDENTITY (1, 1) NOT NULL ,     [Email] [varchar] (100) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,     [Password] [varchar] (10) COLLATE SQL_Latin1_General_CP1_CI_AS NULL ) ON [PRIMARY] GO ALTER TABLE [dbo].[Roles] WITH NOCHECK ADD     CONSTRAINT [PK_Roles] PRIMARY KEY  CLUSTERED     (         [RoleID]     )  ON [PRIMARY] GO ALTER TABLE [dbo].[UserRoleMapping] WITH NOCHECK ADD     CONSTRAINT [PK_UserRoleMapping] PRIMARY KEY  CLUSTERED     (         [MappingID]     )  ON [PRIMARY] GO ALTER TABLE [dbo].[Users] WITH NOCHECK ADD     CONSTRAINT [PK_Users] PRIMARY KEY  CLUSTERED     (         [UserID]     )  ON [PRIMARY] GO  CREATE  INDEX [IX_UserRoleMapping] ON [dbo].[UserRoleMapping]([UserID]) ON [PRIMARY] GO  CREATE  INDEX [IX_Users] ON [dbo].[Users]([Email]) ON [PRIMARY] GO ALTER TABLE [dbo].[UserRoleMapping] ADD     CONSTRAINT [FK_UserRoleMapping_Roles] FOREIGN KEY     (         [RoleID]     ) REFERENCES [dbo].[Roles] (         [RoleID]     ),     CONSTRAINT [FK_UserRoleMapping_Users] FOREIGN KEY     (         [UserID]     ) REFERENCES [dbo].[Users] (         [UserID]     ) GO SET QUOTED_IDENTIFIER ON GO SET ANSI_NULLS OFF GO CREATE PROCEDURE GetRoles(@email varchar(200)) AS declare @UserID int SELECT @UserID = UserID From Users WHERE Email = @Email SELECT Roles.Role FROM Roles, UserRoleMapping WHERE Roles.RoleID =  UserRoleMapping.RoleID and UserRoleMapping.UserID = @UserID GO SET QUOTED_IDENTIFIER OFF GO SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO SET ANSI_NULLS OFF GO CREATE PROCEDURE GetUser(@Email varchar(200), @Password varchar(50)) AS SELECT * FROM Users WHERE Email = @Email AND Password = @Password GO SET QUOTED_IDENTIFIER OFF GO SET ANSI_NULLS ON GO 

The last piece of code you need to write is the code that is responsible for unpacking the list of roles from the FormsAuthenticationTicket and creating a new GenericPrincipal that contains the roles. You will implement this functionality by handling the Application_AuthenticateRequest in global.asax. Listing 7.19 shows this code.

Listing 7.19 The global.asax Containing the Application_AuthenticateRequest Handler
 using System; using System.Collections; using System.ComponentModel; using System.Web; using System.Web.SessionState; namespace DBFormURL {     /// <summary>     /// Summary description for Global.     /// </summary>     public class Global : System.Web.HttpApplication     {         protected void Application_Start(Object sender, EventArgs e)         {             Application["DSN"] = "SERVER=localhost;UID=sa;PWD=;DATABASE= SecuritySample";         }         protected void Application_AuthenticateRequest(object sender, EventArgs e)         {             // Make sure the user has been authenticated             // This event fires for unauthenticated users also             if(Request.IsAuthenticated)             {                 // Get the users identity                 System.Web.Security.FormsIdentity fiUser  = (System.Web.Security.FormsIdentity)User.Identity;                 // Get the ticket                 System.Web.Security.FormsAuthenticationTicket at = fiUser.Ticket;                 // Grab out the roles                 string strRoles = at.UserData;                 // Renew the ticket if need be                 System.Web.Security.FormsAuthenticationTicket ticket = System.Web.Security.FormsAuthentication.RenewTicketIfOld(at);                 if(ticket!=at)                 {                     // Get the encrypted version of the ticket                     string strEncrypted = System.Web.Security.FormsAuthentication.Encrypt(ticket);                     // Put it into a cookie                     HttpCookie hc = new HttpCookie(System.Web.Security. FormsAuthentication.FormsCookieName, strEncrypted);                     hc.Expires = DateTime.Now.AddMinutes(20);                     // Add it to the cookies collection                     Response.Cookies.Add(hc);                 }                 // Create a new principal which includes our role information from the cookie                 HttpContext.Current.User = new System.Security.Principal. GenericPrincipal(fiUser, strRoles.Split(','));             }         }     } } 

In the AuthenticateRequest handler, you first check whether the user has been authenticated yet. If the user has not been authenticated yet, the Identity property of the User object will be null and you will not have a ticket from which to retrieve the role information. After the login.aspx form has authenticated the user, subsequent firings of the AuthenticateRequest event will include an identity. After you know there is an Identity to be had, grab the Identity and cast it to a FormIdentity. The FormIdentity implementation of the IIdentity interface provides a property called Ticket for you to use to retrieve the ticket. After you have the ticket, retrieve the user data containing the role information. The final and most important step is to create a new principal object containing the roles. The last line of the handler creates a new GenericPrincipal and passes a string array of roles that that retrieved from the ticket to it.

for RuBoard


C# Developer[ap]s Guide to ASP. NET, XML, and ADO. NET
C# Developer[ap]s Guide to ASP. NET, XML, and ADO. NET
ISBN: 672321556
EAN: N/A
Year: 2005
Pages: 103

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net