Programming Microsoft Web Forms (Pro Developer)
The BikeBlogSecured Application
An application such as a Web log must often know who is accessing the application. As in most applications, certain tasks such as entering blog entries should only be performed by certain users. The complete source for the BikeBlogSecured Web site is included with the content available for download. (For more information about the downloadable code, see the Introduction.)
While working through the Security Setup Wizard in the ASP.NET Web Site Administration Tool, I entered a single user, named dreilly. After that, I added that user to the admins role. Finally, I set up rules to allow only users in the admins role to access pages in the Admin folder.
By now you might be thinking, "How would a user know to type in the complete URL for a page in my admin folder? I never show the administrator links to non-administrator users. Why do I have to secure the administrator pages?" There are a couple of responses to this. First, never underestimate what users of your system might do, intentionally or unintentionally. Users can often stumble into unsecured portions of the system that you do not intend to expose to them. Second, imagine a scenario in which a user has rights to use the administrative interface on your Web site; then that user leaves the company. If the user has the URL for the administrative interface of your Web site saved in his or her favorites, even if you never directly link to the page, the user might be able to return to the page. Never rely on "security by obscurity."
After I ran the ASP.NET Web Site Administration Tool and created an initial user, I needed to test the ability to log in to the site. The Web site was created with a Master Page used for all pages except Login.aspx. On the Master Page, I added two of the login-related controls, a LoginStatus control and a LoginName control. When the Web site is run, the page looks like Figure 7-19.
Much of this page is the same as previous versions, displaying blog entries in a GridView control. In the upper-right, the LoginStatus control is visible, rendered as the Login link. Clicking the link opens the Login.aspx page, shown in Figure 7-20.
Note that the Login.aspx page does not use the Master Page that all the other pages in the Web site use. I did this to ensure that the Login link does not appear on the login page. The login page contains two of the login-related controls, a Login control and a PasswordRecovery control. It is worth noting that there is no code at all in the code file for Login.aspx.
Look at Figure 7-20 again. The URL in the Address bar has an argument tacked onto it, the ReturnUrl. The value is somewhat obscured by the fact that several special characters in the URL are escaped, or replaced with a substituted value (for example, "/" is replaced by "%2f"), but the argument indicates that when users log in, they will be returned to /BikeBlogSecured/Default.aspx, the page they came from. When users are successfully logged in and returned to Default.aspx, a Logout link is displayed instead of a Login link, and the user name is displayed in the upper-right corner of the page, as shown in Figure 7-21.
If I click the Logout link, I am immediately logged out of the application, and the screen appears as shown in Figure 7-19.
After I am logged in, I might want to add a comment to an entry. To do so, I click the subject link in the GridView control; a details page displays current comments and allows me to add additional comments. Figure 7-22 shows the BlogEntryDetails.aspx page.
BlogEntryDetails.aspx contains a copy of the DisplayBlogEntryUC.ascx user control built in the last chapter and a GridView control to show comments on the current entry. A SqlDataSource control also points to a stored procedure that selects comments for a specified BlogEntryID and returns them sorted in order of DateEntered. The BlogEntryDetails.aspx page takes advantage of a useful feature of the GridView control, the EmptyDataText property that sets text to be displayed when no results are returned. Figure 7-23 shows BlogEntryDetails.aspx when no comments have been entered.
The Add Comment link is visible in Figure 7-22 and Figure 7-23. This link does not appear for anonymous users. This could have been done in several ways. For example, I could have used a LoginView control with nothing in the "logged out" user template and the Add Comment link in the "logged in" user template. Instead, I opted to control the visibility of the link by using code, so that I could demonstrate the programmatic interface to the authentication system. Listing 7-1 shows the code in BlogEntryDetails.aspx.cs.
Listing 7-1: BlogEntryDetails.aspx.cs
using System; using System.Data; using System.Data.SqlClient; using System.Configuration; using System.Collections; using System.Web; using System.Web.Security; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.WebControls.WebParts; using System.Web.UI.HtmlControls; public partial class BlogEntryDetails : System.Web.UI.Page { public int BlogEntryID { get { return ViewState["BlogEntryID"] == null ? 0 : (int)ViewState["BlogEntryID"]; } set { viewState["BlogEntryID"] = value; } } protected void Page_Load(object sender, EventArgs e) { if (this.IsPostBack == false) { int blogEntryID; int.TryParse(Request["BlogEntryID"], out blogEntryID); BlogEntryID = blogEntryID; this.DisplayBlogEntryUC1.BlogEntryID = BlogEntryID; if (User.Identity.IsAuthenticated) { this.hlAddComment.Visible = true; this.hlAddComment.NavigateUrl = "~/users/addcomment.aspx?BlogEntryBACKGROUND-COLOR: #a5cbff"> BlogEntryID.ToString(); } else { this.hlAddComment.Visible = false; } } } }
First, I added a property to hold the BlogEntryID, using a pattern that should be familiar by now, storing the value in view state. In the Page_Load event handler, all code is inside an if block and is only executed when the IsPostBack property is false. I declared an int variable, blogEntryID, the same as the property but with a lowercase character as the first character. I used the blogEntryID variable in the call to the TryParse method because as a property, BlogEntryID cannot be passed directly as the second parameter to TryParse; the second parameter is an out parameter, meaning that the value is modified by the method being called. I set the BlogEntryID property and used that to set the BlogEntryID property of the user control that displays blog entry details.
After I set the BlogEntryID property, I checked to see whether the user is an authenticated user. User.Identity.IsAuthenticated is a bool property that is true if the user has been authenticated and false if not. Remember that simply not exposing a link is not enough to ensure that the link is secured. To ensure that an unauthenticated user does not bypass the login page and simply enter the URL to add a comment, I added a new folder to the application named Users, and then I added the AddComment.aspx page to that folder. I added a rule in the ASP.NET Web Site Administration Tool that denies access to anonymous users, as shown in Figure 7-24.
With this rule in place, even if a user knows the URL for the AddComment.aspx page, he or she can access the Web Form only if authenticated.
When a user is logged in and clicks the Add Comment link, the AddComment.aspx Web Form appears, as shown in Figure 7-25.
AddComment.aspx uses the Master Page, and it has a copy of the DisplayBlogEntryUC.ascx control, as well as subject labels and message text boxes. Most of the code on the AddComment.aspx page is similar to code shown in previous examples. However, one method, btnSave_Click, is entirely new, as shown here.
protected void btnSave_Click(object sender, EventArgs e) { SqlConnection cn = new SqlConnection( ConfigurationManager. ConnectionStrings[ "BikeBlogConnectionString"].ConnectionString); cn.Open(); try { SqlCommand cmd = new SqlCommand("spInsertComment", cn); cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.AddWithValue("@BlogEntryID", BlogEntryID); cmd.Parameters.AddWithValue("@Subject", this.edSubject.Text); cmd.Parameters.AddWithValue("@Message", this.edMessage.Text); cmd.Parameters.AddWithValue("@EnteredBy", User.Identity.Name); cmd.ExecuteNonQuery(); Response.Redirect("~/BlogEntryDetails.aspx?BLogEntryadmins")) { this.hlAddEntry.Visible = true; } else { this.hlAddEntry.Visible = false; } }
The code uses the Users.IsInRole method to determine whether the user is in the admins role. If the user is in the admins role, the hyperlink is shown; otherwise, the hyperlink is invisible.
Many more enhancements could certainly be made to the BikeBlogSecured site as presented here. For example, there is no way for the user to directly edit an entry, although an administrator could easily do so by entering the URL. You could solve this problem by having the BlogEntryDetails.aspx page determine at runtime whether the user is a member of the admins role, and if so, redirect the user to AddEditBlogEntry.aspx. Another possible solution would be to add a column to the GridView control on the main page that allowed members of the admins role to jump directly to the AddEditBlogEntry.aspx page.
As another improvement, the login-related controls that expose a user interface could be templated to blend into the site better than they currently do. Nonetheless, the login-related controls as presented in this chapter allowed me to create a reasonably complete user management system without having to write much code.