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.
The solution involves the following steps:
Implement the solution described in Recipe 8.2, adding to
web.config
the required roles for each of the
pages.
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.
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.
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#).
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:
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.
The second parameter is the user’s name. This can be any string you want to use to identify the user.
The third parameter is the issue date/time of the authentication ticket. This would normally be set to the current date/time.
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.
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
).
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:
If (Context.Request.IsAuthenticated) ThenIf (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) 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.
identity = CType(Context.User.Identity, FormsIdentity) roles = identity.Ticket.UserData 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:
Context.User = New GenericPrincipal(identity, _ roles.Split(","c)) 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:
userName = context.User.Identity.Name userName = context.User.Identity.Name;
To check the user’s role(s), use the following code:
If (Context.User.IsInRole("User")) Then 'perform functions for role End If 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.
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