Pulling It All Together





Pulling It All Together

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 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. 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.


 Python   SQL   Java   php   Perl 
 game development   web development   internet   *nix   graphics   hardware 
 telecommunications   C++ 
 Flash   Active Directory   Windows