Recipe 8.3 Restricting Access to Application Pages by Role

     

8.3.1 Problem

You want to assign or make use of predefined roles for the users of your application, and you want to control access to pages as a function of these roles.

8.3.2 Solution

The solution involves the following steps:

  1. Implement the solution described in Recipe 8.2, adding to web.config the required roles for each of the pages.

  2. In the code-behind class for the ASP.NET login page, add the user's role information to the authentication cookie when the user logs in.

  3. Add code to the Application_AuthenticateRequest method in the global.asax code-behind to recover the user role information and build a user principal object.

  4. Set the user principal object to the Context.User property to provide ASP.NET the data it needs to perform page-by-page authentication.

The code we've written to illustrate this solution appears in Example 8-6 through Example 8-10. The <authentication> and <authorization> elements of web.config are shown in Example 8-6. The login page code-behind where the authentication cookie is created is shown in Example 8-7 (VB) and Example 8-8 (C#). (See Recipe 8.1 for the .aspx file for a typical login page.) The Application_AuthenticateRequest method in the code-behind for global.asax is shown in Example 8-9 (VB) and Example 8-10 (C#).

8.3.3 Discussion

The approach we favor for this recipe builds on Recipe 8.2 but quickly takes a tack of its own based on the addition and use of user roles. The <authentication> and <authorization> elements of the web.config file are identical to those used in Recipe 8.2. And like Recipe 8.2, <location> elements are used to define the access requirements for each page. The <location> elements for the public access pages are also identical.

In this recipe, however, the <location> elements for the restricted pages each contains a list of roles required for access to the page it controls. The following code shows an example. For Home.aspx , the User and Admin roles are allowed access. For AdminPage.aspx , only the " Admin " role is allowed access:

 <location  path ="Home.aspx  "> <system.web> <authorization>  <allow roles="User,   Admin"/>   <deny users="*"/>  </authorization> </system.web> </location> <location  path="AdminPage.aspx  "> <system.web> <authorization>  <allow roles="Admin"/>   <deny users="*"/>  </authorization> </system.web> </location> 

It is important to include the <deny users="*" /> element for all pages after the list of roles allowed to access the page. This informs ASP.NET that if the user is not assigned one of the previously listed roles, they should be denied access to the page.


You might be tempted to use folders to contain pages with similar access rights, as described in Recipe 8.2. However, this approach is much more complicated when roles are used, because a user can be assigned multiple roles and any given page may be accessible by multiple roles. You might be able to initially segment your application to use the folder approach, but later changes will be very difficult. If you are using roles to control access to pages in your application, we recommend that you include a <location> element for each page in your web.config file.


The operations required after the user credentials have been verified are a little different than the previous recipes in this chapter. First, a FormsAuthenticationTicket is created. This authentication ticket will be used as the authentication cookie. By manually creating the ticket, you can add the user role information to the authentication cookie.

To manually create the ticket, you must instantiate a System.Web.Security.FormsAuthenticationTicket object, as shown in a general form here and in a more application-specific form in Example 8-7 (VB) and Example 8-8 (C#):

  ticket = New FormsAuthenticationTicket(   version   , _   name   , _   issueDate   , _   expiration   , _   isPersistent   , _   userData   )  

The ticket takes six parameters:


version

The first parameter is a version number. In our code-behind examples we've used the number 1, but you can use any value. In a production application that allows persistent cookies to be stored on the client, the version number can be used to track and handle changes to the data in the cookie.


name

The second parameter is the user's name. This can be any string you want to use to identify the user.


issueDate

The third parameter is the issue date/time of the authentication ticket. This would normally be set to the current date/time.


expiration

The fourth parameter is the expiration date/time of the authentication ticket. The difference between this value and the issue date/time needs to be greater than or equal to the session timeout value. In Example 8-7 and Example 8-8, it is set to 30 minutes in the future.


isPersistent

The fifth parameter is a flag indicating whether the cookie should be persisted on the client (if set to True ) or should be memory-based (if set to False ).


userData

The sixth parameter can be any string value you want to store with the cookie. It is used in our example to store a comma-delimited list of user roles. This list of roles is used to authorize access to the pages in your application.

Depending on how the role information is stored for a user of your application, you may need to build a comma-delimited string containing the roles and instead use this string as the sixth parameter of the authentication ticket.


After the application creates it, an authentication ticket needs to be converted to an encrypted string. Only strings can be stored in cookies, and encryption prevents the possibility of tampering with the data stored there. To encrypt the string, we call the Encrypt method of the FormsAuthentication class, passing it the ticket that we created. The method returns a string containing the encrypted ticket:

 encryptedStr = FormsAuthentication.Encrypt(ticket) 

Now you need to create a cookie from the encrypted string, using the name of the Forms authentication cookie defined in the web.config file. Naming the cookie anything else will keep the authentication from working, since ASP.NET will look for the cookie by the name defined in web.config and will not find it.

 cookie = New HttpCookie(FormsAuthentication.FormsCookieName, encryptedStr) 

Next , you need to set the expiration date and time for the cookie. If the cookie is to be persisted, the expiration should be set significantly in the future. In Example 8-7 and Example 8-8, we've set the expiration date 10 years in the future. If the cookie is not to be persisted, the expiration date and time should not be set or the cookie will be made persistent.

The last step before redirecting the user to the appropriate next page (the same as in previous recipes in this chapter) is to add the cookie you created to the cookie collection so it will be sent to the client browser on the redirect. This is done by calling the Add method of the Response object's Cookies collection, and passing it the cookie to be added.

Now that the user is logged in and the role information has been added to the authentication cookie, you need to add the Application_AuthenticateRequest method to the code-behind for global.asax (see Example 8-9 and Example 8-10). This method is executed for every requested resource that ASP.NET manages and allows the authentication/authorization process to be customized.

In the Application_AuthenticateRequest method, the first thing you need to do is to check to see if the user is currently authenticated by calling the IsAuthenticated method of the Request object. If not, no action needs to be taken.

If the user is authenticated, you need to check to see if the authentication type is set to Forms . If it is not, an exception should be thrown, since there is a significant mismatch between the code and the authentication cookie. This is illustrated by the following code:

 
figs/vbicon.gif
 If (Context.Request.IsAuthenticated) Then  If (Context.User.Identity.AuthenticationType = "Forms") Then  .. Else  'application is improperly configured so throw an exception   Throw New ApplicationException("Application Must Be Configured For Forms Authentication")  End If 'If (Context.User.Identity.AuthenticationType = "Forms") End If 'If (Context.Request.IsAuthenticated) 
figs/csharpicon.gif
 if (Context.Request.IsAuthenticated) {  if (Context.User.Identity.AuthenticationType == "Forms")  { .. } else {  // application is improperly configured so throw an exception   throw new ApplicationException("Application Must Be Configured For Forms Authentication");  } // if (Context.User.Identity.AuthenticationType = "Forms") } // if (Context.Request.IsAuthenticated) 

Next, you need to get the user roles that you added to the Forms authentication cookie. This is done by getting the identity from the Context.User object. You must cast the identity to a FormsIdentity object to get access to the authentication ticket data where the user roles are stored. This casting is the primary reason why the verification was made on the authentication type. If it was not Forms , this casting would throw a generic exception that would be much harder to troubleshoot than an exception that explicitly states what is wrong. The list of roles retrieved from the UserData property of the authentication ticket is the comma-delimited list of the user roles you added to the authentication ticket during login.

 
figs/vbicon.gif
 identity = CType(Context.User.Identity, FormsIdentity) roles = identity.Ticket.UserData 
figs/csharpicon.gif
 identity = (FormsIdentity)(Context.User.Identity); roles = identity.Ticket.UserData; 

With the user identity and the roles in hand, you now have the information you need to create a GenericPrincipal object and assign it to the User property of the Context object. This GenericPrincipal object is what ASP.NET uses, along with the information in the web.config file, to perform the authorization for the requested page.

The GenericPrincipal constructor requires the user's identity and an array of strings for the roles assigned to the user. The user's identity is the FormsIdentity object you retrieved previously. The comma-delimited list of roles you retrieved previously can easily be converted to an array of strings by using the Split method of the String object, passing a comma as the delimiter used for performing the split. This is illustrated in the following code:

 
figs/vbicon.gif
 Context.User = New GenericPrincipal(identity, _ roles.Split(","c)) 
figs/csharpicon.gif
 Context.User = new GenericPrincipal(identity, roles.Split(',')); 

As with previous recipes in this chapter, you do not need to add any code to the individual pages in your application to handle the authentication and authorization. ASP.NET will now do all of that work for you and let you concentrate on the requirements of the individual pages. If the authorization requirements change for your application and different roles are required to access pages, the only changes required will be to the web.config <location> elements.

Even though you do not need to perform any of the authentication and authorization tasks , you may still want access to the information to customize your pages with the user information or to change what is displayed on pages as a function of the user's roles. The username can be obtained as shown here:

 
figs/vbicon.gif
 userName = context.User.Identity.Name 
figs/csharpicon.gif
 userName = context.User.Identity.Name; 

To check the user's role(s), use the following code:

 
figs/vbicon.gif
 If (Context.User.IsInRole("User")) Then 'perform functions for role End If 
figs/csharpicon.gif
 if (Context.User.IsInRole("User")) { // perform functions for role } 

Using the GenericPrincipal object, no mechanism is provided to get the list of roles assigned to the user. You can only check to see if a user can perform a specific role. If your application requires that you have access to the list of roles, you can store the role information in a Session variable, or you can create a custom user principal inheriting from the GenericPrincipal class and adding the functionality needed by your application.

8.3.4 See Also

Recipe 8.1; Recipe 8.2

Example 8-6. web.config for restricting access by user role
 <?xml version="1.0" encoding="utf-8" ?> <configuration> ..  <authentication mode="Forms">   <forms name=".ASPNETCookbookVB3"   loginUrl="login.aspx"   protection="All"   timeout="30"   path="/">   </forms>   </authentication>   <authorization>   <deny users="*" /> <!-- Deny all users -->   </authorization>  .. <!-- **************************************************************************** The following section provides public access to pages that do not require authentication. An entry must be included for each page that does not require authentication. **************************************************************************** -->  <location path="PublicPage.aspx">   <system.web>   <authorization>   <allow users="*"/>   </authorization>   </system.web>   </location>  <!-- **************************************************************************** The following section defines the pages that require authentication for access. An entry must be included for each page that requires authentication with a list of the roles required for access to the page. Valid Roles are as follows . NOTE: The roles must be entered exactly as listed. User Admin **************************************************************************** -->  <location path="Home.aspx">   <system.web>   <authorization>   <allow roles="User,   Admin"/>   <deny users="*"/>   </authorization>   </system.web>   </location>   <location path="AdminPage.aspx">   <system.web>   <authorization>   <allow roles="Admin"/>   <deny users="*"/>   </authorization>   </system.web>   </location>  </configuration> 

Example 8-7. Login page code-behind (.vb)
 Option Explicit On Option Strict On '----------------------------------------------------------------------------- ' ' Module Name: Login.aspx.vb ' ' Description: This module provides the code behind for the ' Login.aspx page ' '***************************************************************************** Imports Microsoft.VisualBasic Imports System Imports System.Configuration Imports System.Data Imports System.Data.OleDb Imports System.Web Imports System.Web.Security Imports System.Web.UI.HtmlControls Imports System.Web.UI.WebControls Namespace ASPNetCookbook.VBSecurity83 Public Class Login Inherits System.Web.UI.Page 'controls on the form Protected txtLoginID As TextBox Protected txtPassword As TextBox Protected chkRememberMe As CheckBox Protected WithEvents btnLogin As HtmlInputButton '************************************************************************* ' ' ROUTINE: Page_Load ' ' DESCRIPTION: This routine provides the event handler for the page load ' event. It is responsible for initializing the controls ' on the page. '------------------------------------------------------------------------- Private Sub Page_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles MyBase.Load 'Put user code to initialize the page here End Sub 'Page_Load '************************************************************************* ' ' ROUTINE: btnLogin_ServerClick ' ' DESCRIPTION: This routine provides the event handler for the login ' button click event. It is responsible for authenticating ' the user and redirecting to the next page if the user ' is authenticated. '------------------------------------------------------------------------- Private Sub btnLogin_ServerClick(ByVal sender As Object, _ ByVal e As System.EventArgs) _ Handles btnLogin.ServerClick 'name of querystring parameter containing return URL Const QS_RETURN_URL As String = "ReturnURL" Dim dbConn As OleDbConnection Dim dCmd As OleDbCommand Dim dr As OleDbDataReader Dim strConnection As String Dim strSQL As String Dim nextPage As String Dim ticket As FormsAuthenticationTicket Dim cookie As HttpCookie Dim encryptedStr As String Try 'get the connection string from web.config and open a connection 'to the database strConnection = _ ConfigurationSettings.AppSettings("dbConnectionString") dbConn = New OleDb.OleDbConnection(strConnection) dbConn. Open ( ) 'check to see if the user exists in the database strSQL = "SELECT (FirstName + ' ' + LastName) AS UserName, " & _ "Role " & _ "FROM AppUser " & _ "WHERE LoginID=? AND " & _ "Password=?" dCmd = New OleDbCommand(strSQL, dbConn) dCmd.Parameters.Add(New OleDbParameter("LoginID", _ txtLoginID.Text)) dCmd.Parameters.Add(New OleDbParameter("Password", _ txtPassword.Text)) dr = dCmd.ExecuteReader( ) If (dr.Read( )) Then  'user credentials were found in the database so notify the system   'that the user is authenticated   'create an authentication ticket for the user with an expiration   'time of 30 minutes and placing the user's role in the userData   'property   ticket = New FormsAuthenticationTicket(1, _   CStr(dr.Item("UserName")), _   DateTime.Now( ), _   DateTime.Now( ).AddMinutes(30), _   chkRememberMe.Checked, _   CStr(dr.Item("Role")))   encryptedStr = FormsAuthentication.Encrypt(ticket)   'add the encrypted authentication ticket in the cookies collection   'and if the cookie is to be persisted, set the expiration for   '10 years from now. Otherwise do not set the expiration or the   'cookie will be created as a persistent cookie.   cookie = New HttpCookie(FormsAuthentication.FormsCookieName, _   encryptedStr)   If (chkRememberMe.Checked) Then   cookie.Expires = ticket.IssueDate.AddYears(10)   End If   Response.Cookies.Add(cookie)  'get the next page for the user If (Not IsNothing(Request.QueryString(QS_RETURN_URL))) Then 'user attempted to access a page without logging in so redirect 'them to their originally requested page nextPage = Request.QueryString(QS_RETURN_URL) Else 'user came straight to the login page so just send them to the 'home page nextPage = "Home.aspx" End If 'Redirect user to the next page 'NOTE: This must be a Response.Redirect to write the cookie to the ' user's browser. Do NOT change to Server.Transfer which ' does not cause around trip to the client browser and thus ' will not write the authentication cookie to the client ' browser. Response.Redirect(nextPage, True) Else 'user credentials do not exist in the database - in a production 'application this should output an error message telling the user 'that the login ID or password was incorrect End If Finally 'cleanup If (Not IsNothing(dr)) Then dr.Close( ) End If If (Not IsNothing(dbConn)) Then dbConn.Close( ) End If End Try End Sub 'btnLogin_ServerClick End Class 'Login End Namespace 

Example 8-8. Login page code-behind (.cs)
 //---------------------------------------------------------------------------- // // Module Name: Login.aspx.cs // // Description: This module provides the code behind for the // Login.aspx page // //**************************************************************************** using System; using System.Configuration; using System.Data; using System.Data.OleDb; using System.Web; using System.Web.Security; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; namespace ASPNetCookbook.CSSecurity83 { public class Login : System.Web.UI.Page { // controls on the form protected System.Web.UI.WebControls.TextBox txtLoginID; protected System.Web.UI.WebControls.TextBox txtPassword; protected System.Web.UI.WebControls.CheckBox chkRememberMe; protected System.Web.UI.HtmlControls.HtmlInputButton btnLogin; //************************************************************************ // // ROUTINE: Page_Load // // DESCRIPTION: This routine provides the event handler for the page // load event. It is responsible for initializing the // controls on the page. //------------------------------------------------------------------------ private void Page_Load(object sender, System.EventArgs e) { // wire the login button this.btnLogin.ServerClick += new EventHandler(this.btnLogin_ServerClick); } // Page_Load //************************************************************************ // // ROUTINE: btnLogin_ServerClick // // DESCRIPTION: This routine provides the event handler for the login // button click event. It is responsible for // authenticating the user and redirecting to the next // page if the user is authenticated. //------------------------------------------------------------------------ private void btnLogin_ServerClick(Object sender, System.EventArgs e) { // name of querystring parameter containing return URL const String QS_RETURN_URL = "ReturnURL"; OleDbConnection dbConn = null; OleDbCommand dCmd = null; OleDbDataReader dr = null; String strConnection = null; String strSQL = null; String nextPage = null; FormsAuthenticationTicket ticket = null; HttpCookie cookie = null; String encryptedStr = null; try { // get the connection string from web.config and open a connection // to the database strConnection = ConfigurationSettings.AppSettings["dbConnectionString"]; dbConn = new OleDbConnection(strConnection); dbConn.Open( ); // check to see if the user exists in the database strSQL = "SELECT (FirstName + ' ' + LastName) AS UserName, " + "Role " + "FROM AppUser " + "WHERE LoginID=? AND " + "Password=?"; dCmd = new OleDbCommand(strSQL, dbConn); dCmd.Parameters.Add(new OleDbParameter("LoginID", txtLoginID.Text)); dCmd.Parameters.Add(new OleDbParameter("Password", txtPassword.Text)); dr = dCmd.ExecuteReader( ); if (dr.Read( )) {  // user credentials were found in the database so notify the system   // that the user is authenticated   // create an authentication ticket for the user with an expiration   // time of 30 minutes and placing the user's role in the userData   // property   ticket = new FormsAuthenticationTicket(1,   (String)(dr["UserName"]),   DateTime.Now,   DateTime.Now.AddMinutes(30),   chkRememberMe.Checked,   (String)(dr["Role"]));   encryptedStr = FormsAuthentication.Encrypt(ticket);   // add the encrypted authentication ticket in the cookies collection   // and if the cookie is to be persisted, set the expiration for   // 10 years from now. Otherwise do not set the expiration or the   // cookie will be created as a persistent cookie.   cookie = new HttpCookie(FormsAuthentication.FormsCookieName,   encryptedStr);   if (chkRememberMe.Checked)   {   cookie.Expires = ticket.IssueDate.AddYears(10);   }   Response.Cookies.Add(cookie);  // get the next page for the user if (Request.QueryString[QS_RETURN_URL] != null) { // user attempted to access a page without logging in so redirect // them to their originally requested page nextPage = Request.QueryString[QS_RETURN_URL]; } else { // user came straight to the login page so just send them to the // home page nextPage = "Home.aspx"; } // Redirect user to the next page // NOTE: This must be a Response.Redirect to write the cookie to // the user's browser. Do NOT change to Server.Transfer // which does not cause around trip to the client browser // and thus will not write the authentication cookie to the // client browser. Response.Redirect(nextPage, true); } else { // user credentials do not exist in the database - in a production //application this should output an error message telling the user // that the login ID or password was incorrect. } } // try finally { // cleanup if (dr != null) { dr.Close( ); } if (dbConn != null) { dbConn.Close( ); } } // finally } // btnLogin_ServerClick } // Login } 

Example 8-9. Application_AuthenticateRequest method in global.asax.vb
 Sub Application_AuthenticateRequest(ByVal sender As Object, _ ByVal e As EventArgs) Dim roles As String Dim identity As FormsIdentity If (Context.Request.IsAuthenticated) Then If (Context.User.Identity.AuthenticationType = "Forms") Then 'get the comma delimited list of roles from the user data 'in the authentication ticket identity = CType(Context.User.Identity, FormsIdentity) roles = identity.Ticket.UserData 'create a new user principal object with the current user identity 'and the roles assigned to the user Context.User = New GenericPrincipal(identity, _ roles.Split(",")) Else 'application is improperly configured so throw an exception Throw New ApplicationException("Application Must Be Configured For Forms Authentication") End If 'If (Context.User.Identity.AuthenticationType = "Forms") End If 'If (Context.Request.IsAuthenticated) End Sub 'Application_AuthenticateRequest 

Example 8-10. Application_AuthenticateRequest method in global.asax.cs
 protected void Application_AuthenticateRequest(Object sender, EventArgs e) { String roles = null; FormsIdentity identity = null; if (Context.Request.IsAuthenticated) { if (Context.User.Identity.AuthenticationType == "Forms") { // get the comma delimited list of roles from the user data // in the authentication ticket identity = (FormsIdentity)(Context.User.Identity); roles = identity.Ticket.UserData; // create a new user principal object with the current user identity // and the roles assigned to the user Context.User = new GenericPrincipal(identity, roles.Split(',')); } else { // application is improperly configured so throw an exception throw new ApplicationException("Application Must Be Configured For Forms Authentication"); } // if (Context.User.Identity.AuthenticationType = "Forms") } // if (Context.Request.IsAuthenticated) } // Application_AuthenticateRequest 



ASP. NET Cookbook
ASP.Net 2.0 Cookbook (Cookbooks (OReilly))
ISBN: 0596100647
EAN: 2147483647
Year: 2006
Pages: 179

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