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.
<?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.
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.
<%@ 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>
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.
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.
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 |