Forms Authentication

Forms authentication is one of ASP.NET’s coolest new features. Simply put, forms authentication is a security mechanism that authenticates a user by asking him or her to type credentials (typically a user name and a password) into a Web form. Through entries in Web.config, you identify the login page and tell ASP.NET which resources the login page protects. The first time a user attempts to access a protected resource, ASP.NET transparently redirects him or her to your login page. If the login is successful, ASP.NET then issues the user an authentication ticket in the form of a cookie and redirects the user to the page originally requested. The ticket allows that user to revisit protected portions of your site without having to log in again and again. You control the ticket’s lifetime, so you decide how long the login is good for.

Forms authentication replaces reams of code in ASP applications that checks (often at the top of every page) to see whether a user has logged in, manually redirects the user to a login page if the answer is negative, and then redirects the user to the page originally requested following a successful login. It’s perfect for enacting the kind of authentication featured on sites like eBay, where you have to type in a user name and password before viewing personalized pages or placing bids on auction items. It also plays well on the Internet, where Windows authentication is seldom practical.

A First Look at Forms Authentication

Just how easy is forms authentication? Check out the application in Figure 10-9 and you be the judge. The application’s user interface consists of two pages: PublicPage.aspx, which can be viewed by anyone, and ProtectedPage.aspx, which is available only to authenticated users. “Authenticated users” means anyone who has logged in through a third page, LoginPage.aspx, which asks for a user name and a password. Valid user names and passwords are stored in Web.config.

Before you dive into the source code, take the application for a test drive. Here’s how:

  1. Copy PublicPage.aspx, LoginPage.aspx, and Web.config (application root) to wwwroot or the virtual directory of your choice.

  2. Create a subdirectory named Secret in the virtual root, and then copy ProtectedPage.aspx and Web.config (Secret subdirectory) to the Secret subdirectory.

  3. Call up PublicPage.aspx in your browser. If you copied it to wwwroot, the proper URL is http://localhost/publicpage.aspx.

  4. Click the View Secret Message button.

  5. View Secret Message uses Response.Redirect to go to Secret/ProtectedPage.aspx. But because ProtectedPage.aspx is viewable only by authenticated users, ASP.NET displays the login form in LoginPage.aspx (Figure 10-7).

  6. Type “Jeff” into the user name field and “imbatman” into the password field.

  7. ProtectedPage.aspx appears. Because you’re now an authenticated user, you’ve been issued an authentication ticket that accompanies subsequent requests as a cookie.

  8. Go back to PublicPage.aspx.

  9. Click the View Secret Message button again.

  10. ProtectedPage.aspx appears again, this time without asking you for a user name and password. Why? Because the authentication cookie transmitted with the request identified you to ASP.NET’s forms authentication module (which listens in on every request) as an authenticated user and even identified you as “Jeff.” Note the personalized greeting on the page (Figure 10-8).

  11. Close your browser. Start it again, and then call up PublicPage.aspx.

  12. Click View Secret Message once more. You’re asked to log in again because the cookie containing your authentication ticket is a session cookie, which means it’s destroyed when you close your browser.

    Figure 10-7
    Forms authentication login form.
    Figure 10-8
    The secret message.

What did it take to prevent unauthenticated users from seeing ProtectedPage.aspx and to direct them to our login form when they attempt to call it up? Not a lot, really. The secret lies in Web.config—to be specific, in the two Web.config files that accompany this application. The Web.config file in the application root enables forms authentication and identifies the login page:

<authentication?mode="Forms">
??<forms?loginUrl="LoginPage.aspx">
????.
????.
????.
??</forms>
</authentication>

It also contains a credentials section listing valid user names and passwords:

<credentials?passwordFormat="Clear">
??<user?name="Jeff" password="imbatman" />
??<user?name="John" password="redrover" />
??<user?name="Bob" password="mxyzptlk" />
??<user?name="Alice" password="nomalice" />
??<user?name="Mary" password="contrary" />
</credentials>

The Web.config file in the Secret subdirectory plays an equally important role in securing the application. In it, the statements

<authorization>
??<deny?users="?" />
</authorization>

denote a URL authorization. They instruct ASP.NET’s URL authorization module (System.Web.Security.UrlAuthorizationModule) to deny unauthenticated users access to any ASP.NET files in the host directory. The “?” stands for anonymous users, which is another way of saying unauthenticated users. When someone attempts to view a file in this directory, ASP.NET checks to see whether a valid authentication cookie is attached to the request. If the cookie exists, ASP.NET unencrypts it, validates it to ensure that it hasn’t been tampered with, and extracts identity information that it assigns to the current request. (Encryption and validation can be turned off but are enabled by default.) If the cookie doesn’t exist, ASP.NET redirects the request to the login page.

The actual authentication—soliciting a user name and password and checking their validity—is performed by LoginPage.aspx. The following statement passes the user name and password that the user entered to the static System.Web.Security.FormsAuthentication method named Authenticate, which returns true if the user name and password are valid (that is, if they appear in the credentials section of Web.config) and false if they’re not:

if?(FormsAuthentication.Authenticate?(UserName.Text,?Password.Text))

If Authenticate returns true, the next statement creates an authentication cookie, attaches it to the outgoing response, and redirects the user to the page that he or she originally requested:

FormsAuthentication.RedirectFromLoginPage?(UserName.Text,?false);

The second parameter passed to RedirectFromLoginPage specifies whether the authentication should be a session cookie (false) or a persistent cookie (true). Many sites that use forms authentication present the user with a check box that lets him or her decide which type of cookie to issue. If you see a check box labeled “Keep me logged in on this site” or something to that effect, checking the box generally issues an authentication cookie whose lifetime is independent of the browser session.

ProtectedPage.aspx is the only ASPX file in the Secret subdirectory, but if there were others, they too would be protected by the login form. Protection is applied on a directory-by-directory basis. Applying two different protection levels to two sets of files requires hosting those files in separate directories. Web.config files in each directory specify exactly how the files are to be protected.

PublicPage.aspx
<html>
??<body>
????<h1>Public?Page</h1>
????<hr>
????<form?runat="server">
??????<asp:Button?Text="View?Secret?Message" OnClick="OnViewSecret"
????????RunAt="server" />
????</form>
??</body>
</html>

<script?language="C#" runat="server">
??void?OnViewSecret?(Object?sender,?EventArgs?e)
??{
??????Response.Redirect?("Secret/ProtectedPage.aspx");
??}
</script>
Figure 10-9
Simple forms authentication.
ProtectedPage.aspx
<%@?Page?Language="C#" %>

<html>
??<body>
????<h1>Protected?Page</h1>
????<hr><br>
????<%?Response.Write?(Context.User.Identity.Name?+ ": ");?%>
????Be?careful?investing?your?money?in?dot-coms.
??</body>
</html>
LoginPage.aspx
<html>
??<body>
????<h1>Please?Log?In</h1>
????<hr>
????<form?runat="server">
??????<table?cellpadding="8">
????????<tr>
??????????<td>
????????????User?Name:
??????????</td>
??????????<td>
????????????<asp:TextBox?ID="UserName" RunAt="server" />
??????????</td>
????????</tr>
????????<tr>			
??????????<td>
????????????Password:
??????????</td>
??????????<td>
????????????<asp:TextBox?ID="Password" TextMode="password"
??????????????RunAt="server" />
??????????</td>
????????</tr>
????????<tr>
??????????<td>
????????????<asp:Button?Text="Log?In" OnClick="OnLogIn"
??????????????RunAt="server" />
??????????</td>
??????????<td>
??????????</td>
????????</tr>
??????</table>
????</form>
????<hr>
????<h3><asp:Label?ID="Output" RunAt="server" /></h3>
??</body>
</html>

<script?language="C#" runat="server">
??void?OnLogIn?(Object?sender,?EventArgs?e)
??{
??????if?(FormsAuthentication.Authenticate?(UserName.Text,
??????????Password.Text))
??????????FormsAuthentication.RedirectFromLoginPage?(UserName.Text,
??????????????false);
??????else
??????????Output.Text?= "Invalid?login";
??}
</script>
Web.config (Application Root)
<configuration>
??<system.web>
????<authentication?mode="Forms">
??????<forms?loginUrl="LoginPage.aspx">
????????<credentials?passwordFormat="Clear">
??????????<user?name="Jeff" password="imbatman" />
??????????<user?name="John" password="redrover" />
??????????<user?name="Bob" password="mxyzptlk" />
??????????<user?name="Alice" password="nomalice" />
??????????<user?name="Mary" password="contrary" />
????????</credentials>
??????</forms>
????</authentication>
??</system.web>
</configuration>
Web.config (Secret Subdirectory)
<configuration>
??<system.web>
????<authorization>
??????<deny?users="?" />
????</authorization>
??</system.web>
</configuration>
Real-World Forms Authentication

The application in the previous section isn’t very realistic for a couple of reasons. First, it’s unreasonable to store passwords in clear text. ASP.NET has a fix for that, but I won’t even mention it here because it becomes a moot point in light of problem number two—namely, that storing thousands (or hundreds of thousands) of names and passwords in Web.config is completely unrealistic. In the real world, you’d store that information in a database. Storing user names and passwords in a database and still leveraging forms authentication is precisely what this section is about.

Figure 10-11 lists a modified version of the application that stores user names and passwords in a Microsoft SQL Server database named WebLogin. The database’s “Users” table contains a list of user names and passwords (Figure 10-10). Only two source code files—LoginPage.aspx and Web.config (the one in the application root)—changed; the others are exactly the same, so they don’t appear in the listing. Web.config no longer has a credentials section containing user names and passwords. LoginPage.aspx no longer uses FormsAuthentication.Authenticate to validate user credentials. Instead, it calls a local method named CustomAuthenticate, which uses an SQL query to determine whether the credentials are valid. If the user types “Jeff” into the user name field and “imbatman” into the password field, the query looks like this:

select?count?(*)?from?users?where?username?=?'Jeff'?and
cast?(rtrim?(password)?as?varbinary)?=?cast?('imbatman'?as?varbinary)

This query returns a count of the number of records containing “Jeff” in the “UserName” field and “imbatman” in the “Password” field. A return value of 1 means the credentials are valid. A 0 return means they’re invalid because no such record exists in the database.

The purpose of the CAST operators in the query is to make the password comparison case-sensitive. By default, most SQL databases ignore case when performing string comparisons. Casting strings to varbinaries has SQL treat them as binary values rather than strings and is a commonly used trick for making string comparisons case-sensitive. The RTRIM operator applied to the Password field strips trailing spaces from the string. SQL ignores trailing spaces when comparing strings but not when performing binary comparisons. Casting the password to varbinary also prevents spoofing with passwords that are actually SQL commands. (At least I think it prevents spoofing; you never know what clever work-arounds evildoers might devise. To be certain, slap RegularExpressionValidators on the TextBox controls to reject input containing anything besides letters and numbers. For good form, throw in a couple of RequireFieldValidators too.)

Figure 10-10
The WebLogin database.

This version of LoginPage.aspx has one other feature that the previous version did not: a check box that lets the user decide whether the authentication cookie issued to him or her is temporary or persistent. LoginPage.aspx passes the value of the check box’s Checked property to RedirectFromLoginPage:

FormsAuthentication.RedirectFromLoginPage?(UserName.Text,
????Persistent.Checked);

Checking the box produces a persistent authentication cookie by passing true to RedirectFromLoginPage, and leaving the box unchecked produces a temporary (session) authentication cookie by passing false. Check the box before logging in and you’ll be able to get back to ProtectedPage.aspx without logging in again, even if you shut down your machine and don’t come back until days later.

Before testing the new version of the application, you must create the WebLogin database. The CD included with this book contains a script named WebLogin.sql that creates it for you. Simply open a command prompt window, go to the directory where WebLogin.sql is stored, and type

osql?-U?sa?-P?-i?weblogin.sql

The installation script will work, of course, only if Microsoft SQL Server is installed on your PC.

LoginPage.aspx
<%@?Import?NameSpace="System.Data.SqlClient" %>

<html>
??<body>
????<h1>Please?Log?In</h1>
????<hr>
????<form?runat="server">
??????<table?cellpadding="8">
????????<tr>
??????????<td>
????????????User?Name:
??????????</td>
??????????<td>
????????????<asp:TextBox?ID="UserName" RunAt="server" />
??????????</td>
????????</tr>
????????<tr>			
??????????<td>
????????????Password:
??????????</td>
??????????<td>
????????????<asp:TextBox?ID="Password" TextMode="password"
??????????????RunAt="server" />
??????????</td>
????????</tr>
????????<tr>
??????????<td>
????????????<asp:Button?Text="Log?In" OnClick="OnLogIn"
??????????????RunAt="server" />
??????????</td>
??????????<td>
????????????<asp:CheckBox?Text="Keep?me?signed?in" ID="Persistent"
??????????????RunAt="server" />
??????????</td>
????????</tr>
??????</table>
????</form>
????<hr>
????<h3><asp:Label?ID="Output" RunAt="server" /></h3>
??</body>
</html>

<script?language="C#" runat="server">
??void?OnLogIn?(Object?sender,?EventArgs?e)
??{
??????if?(CustomAuthenticate?(UserName.Text,?Password.Text))
??????????FormsAuthentication.RedirectFromLoginPage?(UserName.Text,
??????????????Persistent.Checked);
??????else
??????????Output.Text?= "Invalid?login";
??}

??bool?CustomAuthenticate?(string?username,?string?password)
??{
??????SqlConnection?connection?=?new?SqlConnection
??????????("server=localhost;database=weblogin;uid=sa;pwd=");

??????try?{
??????????connection.Open?();

??????????StringBuilder?builder?=?new?StringBuilder?();
??????????builder.Append?("select?count?(*)?from?users " +
????????????? "where?username?=?\'");
??????????builder.Append?(username);
??????????builder.Append?("\'?and?cast?(rtrim?(password)?as " +
????????????? "varbinary)?=?cast?(\'");
??????????builder.Append?(password);
??????????builder.Append?("\'?as?varbinary)");
??????????SqlCommand?command?=?new?SqlCommand?(builder.ToString?(),
??????????????connection);

??????????int?count?=?(int)?command.ExecuteScalar?();
??????????return?(count?>?0);
??????}
??????catch?(SqlException)?{
??????????return?false;
??????}
??????finally?{
??????????connection.Close?();
??????}
??}
</script>
Figure 10-11
Forms authentication utilizing user names and passwords stored in a database.
Web.config (Application Root)
<configuration>
??<system.web>
????<authentication?mode="Forms">
??????<forms?loginUrl="LoginPage.aspx" />
????</authentication>
??</system.web>
</configuration>
Authentication Cookie Lifetime

When you call RedirectFromLoginPage and pass false in the second parameter, ASP.NET issues a session authentication cookie containing a time stamp that limits the cookie’s validity to 30 minutes, even if the browser session extends longer than that. The time-out value of 30 minutes is controlled by the timeout attribute attached to the <forms> element in Machine.config:

<forms?...?timeout="30">

You can change the time-out by editing Machine.config or including a timeout attribute in a local Web.config file. The following Web.config file enables forms authentication and extends the validity of the authentication cookie to 7 days (10,080 minutes):

<configuration>
??<system.web>
????<authentication?mode="Forms">
??????<forms?loginUrl="/LoginPage.aspx" timeout="10080" />
????</authentication>
??</system.web>
</configuration>

When a session time-out cookie is returned to ASP.NET in a subsequent request, ASP.NET automatically renews it (updates the time stamp) if the cookie’s lifetime is more than half over. Thus, even the default time-out of 30 minutes enables you to access a protected page indefinitely as long as the browser remains open and you submit the cookie to ASP.NET at least once every 15 minutes.

If the user checks the “Keep me signed in” box in the login page of the application in the previous section, LoginPage.aspx issues a persistent authentication cookie by passing true to RedirectFromLoginPage. Here’s that statement again:

FormsAuthentication.RedirectFromLoginPage?(UserName.Text,
????Persistent.Checked);

One drawback to issuing a persistent authentication cookie this way is that the cookie remains valid for 50 years. Furthermore, there is no configuration setting that lets you change this. The timeout attribute has no effect on a persistent authentication cookie. Suppose you’d like to issue a persistent authentication cookie but you’d also like to limit its lifetime to, say, 7 days. How do you go about it?

The solution is to programmatically modify the authentication cookie before returning it in the response. Here’s a modified version of OnLogIn (the handler that’s called when the user clicks LoginPage.aspx’s Log In button) that sets the authentication cookie’s lifetime to 7 days—provided, of course, the cookie is a persistent cookie:

void?OnLogIn?(Object?sender,?EventArgs?e)
{
????if?(CustomAuthenticate?(UserName.Text,?Password.Text))?{
????????string?url?=?FormsAuthentication.GetRedirectUrl
????????????(UserName.Text,?Persistent.Checked);

????????FormsAuthentication.SetAuthCookie?(UserName.Text,
????????????Persistent.Checked);

????????if?(Persistent.Checked)?{
????????????HttpCookie?cookie?=
????????????????Response.Cookies[FormsAuthentication.FormsCookieName];
????????????cookie.Expires?=?DateTime.Now?+?new?TimeSpan?(7,?0,?0,?0);
????????}

????????Response.Redirect?(url);
????}
????else
????????Output.Text?= "Invalid?login";
}

If CustomAuthenticate returns true, indicating that the user entered valid credentials, this version of OnLogIn uses FormsAuthentication.GetRedirectUrl to grab the URL of the page that the user originally requested. Then it calls Forms-Authentication.SetAuthCookie to create an authentication cookie and add it to the cookies going out in the response. Before calling Response.Redirect to go to the requested page, however, OnLogIn modifies the cookie by retrieving it from the response’s Cookies collection and setting its Expires property to a date 7 days hence. This simple modification ensures that the user will have to go through your login page again after 7 days. Of course, you can set the lifetime to any length you want by modifying the TimeSpan value. You’ll see this technique used in the chapter’s final sample program. But first, there’s one more topic we need to cover: role-based security.

Forms Authentication and Role-Based Security

The last sample program demonstrated how to combine forms authentication with user names and passwords stored in a SQL Server database. The next one demonstrates how to use role membership to allow some users to view ProtectedPage.aspx while hiding it from others.

The following statement in the Secret directory’s Web.config file prevents unauthenticated users from accessing ASPX files in that directory:

<deny?users="?" />

The only problem with this statement is that it allows any authenticated user to view ProtectedPage.aspx. It’s not unrealistic to imagine that in some scenarios, you might want to allow some authenticated users to view ProtectedPage.aspx without permitting all authenticated users to view it. Suppose John and Alice are managers who should be able to call up ProtectedPage.aspx, but Jeff, Bob, and Mary are mere developers who should not. One way to keep Jeff, Bob, and Mary out is to deny access to all users (users=“*”) but specifically allow access to John and Alice. Here’s a Web.config file that does just that:

<configuration>
??<system.web>
????<authorization>
??????<allow?users="John,?Alice" />
??????<deny?users="*" />
????</authorization>
??</system.web>
</configuration>

Another way to do it is to specifically deny access to Jeff, Bob, and Mary:

<configuration>
??<system.web>
????<authorization>
??????<deny?users="Jeff,?Bob,?Mary" />
??????<allow?users="*" />
????</authorization>
??</system.web>
</configuration>

Be aware that when you use <allow> and <deny> in this manner, the entries are order-sensitive. The statements

<deny?users="*" />
<allow?users="John,?Alice" />

are not equivalent to

<allow?users="John,?Alice" />
<deny?users="*" />

because ASP.NET will stop at <deny users=“*”> and ignore any statements that appear after it.

These Web.config files work just fine, but they’re not very practical for sites that serve large volumes of users. Just imagine what a nightmare it would be to edit multimegabyte Web.config files every time someone enters or leaves your company or gets a promotion. For large sites, roles provide a practical solution to the problem of granting access to some authenticated users without granting access to all of them. And roles work well with forms authentication provided you’re willing to write a little code to help out.

Look again at the WebLogin database that serves our site. In addition to storing user names and passwords, the “Users” table has a field named “Role” that stores each user’s role membership, if any. John and Alice are assigned manager roles, while Jeff, Bob, and Mary are assigned the role of developer. Is it possible to use these role memberships to grant John and Alice—and anyone else assigned the role of manager—access to ProtectedPage.aspx while keeping others away? You bet. All it requires is two simple modifications to the code you’ve already written.

The first step is the easy one. It involves editing the Secret directory’s Web.config file to grant access to managers or deny access to developers. Here’s a Web.config file that does the former:

<configuration>
??<system.web>
????<authorization>
??????<allow?roles="Manager" />
??????<deny?users="*" />
????</authorization>
??</system.web>
</configuration>

The roles attribute takes the place of the users attribute and grants or denies access not to individual users, but to groups of users based on the role or roles that they’ve been assigned.

The second step is more involved. Somehow we have to map the roles stored in the database to user accounts in each and every request so that ASP.NET can determine whether the requestor is a manager or a developer. The best place to do the mapping is in the AuthenticateRequest events that fire at the beginning of every request. You can process AuthenticateRequest events in a custom HTTP module or in Global.asax. Here’s a Global.asax file that layers roles onto forms authentication:

<%@?Import?Namespace="System.Security.Principal" %>

<script?language="C#" runat="server">
??void?Application_AuthenticateRequest?(Object?sender,?EventArgs?e)
??{
??????HttpApplication?app?=?(HttpApplication)?sender;
??????if?(app.Request.IsAuthenticated?&&
??????????app.User.Identity?is?FormsIdentity)?{
??????????FormsIdentity?identity?=?(FormsIdentity)?app.User.Identity;
??????????if?(identity.Name?== "Jeff")
??????????????app.Context.User?=?new?GenericPrincipal?(identity,
??????????????????new?string[]?{ "Developer" });
??????}
??}
</script>

How does it work? After verifying that the user has indeed been authenticated (for forms authentication, “is authenticated” means a valid authentication cookie is attached to the request) and that the authentication was performed using forms authentication, Application_AuthenticateRequest extracts the user name from the cookie. It doesn’t touch the cookie directly; instead, it casts User.Identity to FormsIdentity, which works fine as long as the user was authenticated using forms authentication, and reads the user name from the Forms-Identity object’s Name property.

If the user name is “Jeff,” Application_AuthenticateRequest creates a new GenericPrincipal object containing the role name “Developer” and assigns it to the current request by writing it to the User property of the request’s HttpContext. GenericPrincipal is a device for representing user identities independent of the authentication protocol being used. When code executed in this request attempts to redirect to ProtectedPage.aspx, ASP.NET compares the role name in the GenericPrincipal to the roles granted access through Web.config. Since Jeff is a developer but the Secret directory’s Web.config file allows access only to managers, Jeff is denied access to ProtectedPage.aspx. But change the statement

app.Context.User?=?new?GenericPrincipal?(identity,
????new?string[]?{ "Developer" });

to

app.Context.User?=?new?GenericPrincipal?(identity,
????new?string[]?{ "Manager" });

and Jeff is able to view ProtectedPage.aspx just fine.

Figure 10-12 contains the third and final version of our PublicPage/ProtectedPage application. It includes three features that the previous version did not:

To experience role-based security in action, click PublicPage.aspx’s View Secret Message button and type “Jeff” and “imbatman” into the login form. Because Jeff is identified as a developer in the database, you won’t be able to view ProtectedPage.aspx. But log in as John (password “redrover”), and you’ll pull up ProtectedPage.aspx just fine. Why? Because John’s role is manager, and managers are specifically allowed to access resources in the Secrets directory.

By the way, if clicking the View Secret Message button bypasses the login form and goes straight to ProtectedPage.aspx, that’s because the cookie you were issued when you tested the previous version of the application still identifies you as an authenticated user. If it’s a session cookie, simply restarting your browser will destroy the cookie and let you see the login page again. If it’s a persistent cookie, you’ll have to delete it. The easiest way to do that is to use your browser’s delete cookies command. In Internet Explorer 6.0, you’ll find it under Tools/Internet Options.

LoginPage.aspx
<%@?Import?NameSpace="System.Data.SqlClient" %>

<html>
??<body>
????<h1>Please?Log?In</h1>
????<hr>
????<form?runat="server">
??????<table?cellpadding="8">
????????<tr>
??????????<td>
????????????User?Name:
??????????</td>
??????????<td>
????????????<asp:TextBox?ID="UserName" RunAt="server" />
??????????</td>
????????</tr>
????????<tr>			
??????????<td>
????????????Password:
??????????</td>
??????????<td>
????????????<asp:TextBox?ID="Password" TextMode="password"
??????????????RunAt="server" />
??????????</td>
????????</tr>
????????<tr>
??????????<td>
????????????<asp:Button?Text="Log?In" OnClick="OnLogIn"
??????????????RunAt="server" />
??????????</td>
??????????<td>
????????????<asp:CheckBox?Text="Keep?me?signed?in" ID="Persistent"
??????????????RunAt="server" />
??????????</td>
????????</tr>
??????</table>
????</form>
????<hr>
????<h3><asp:Label?ID="Output" RunAt="server" /></h3>
??</body>
</html>

<script?language="C#" runat="server">
??void?OnLogIn?(Object?sender,?EventArgs?e)
??{
??????if?(CustomAuthenticate?(UserName.Text,?Password.Text))?{
??????????string?url?=?FormsAuthentication.GetRedirectUrl
??????????????(UserName.Text,?Persistent.Checked);

??????????FormsAuthentication.SetAuthCookie?(UserName.Text,
??????????????Persistent.Checked);

??????????if?(Persistent.Checked)?{
??????????????HttpCookie?cookie?=
????????????????Response.Cookies[FormsAuthentication.FormsCookieName];
??????????????cookie.Expires?=?DateTime.Now?+
??????????????????new?TimeSpan?(7,?0,?0,?0);
??????????}

??????????Response.Redirect?(url);
??????}
??????else
??????????Output.Text?= "Invalid?login";
??}

??bool?CustomAuthenticate?(string?username,?string?password)
??{
??????SqlConnection?connection?=?new?SqlConnection
??????????("server=localhost;database=weblogin;uid=sa;pwd=");

??????try?{
??????????connection.Open?();

??????????StringBuilder?builder?=?new?StringBuilder?();
??????????builder.Append?("select?count?(*)?from?users " +
????????????? "where?username?=?\'");
??????????builder.Append?(username);
??????????builder.Append?("\'?and?cast?(rtrim?(password)?as " +
????????????? "varbinary)?=?cast?(\'");
??????????builder.Append?(password);
??????????builder.Append?("\'?as?varbinary)");

??????????SqlCommand?command?=?new?SqlCommand?(builder.ToString?(),
??????????????connection);
??????????int?count?=?(int)?command.ExecuteScalar?();
??????????return?(count?>?0);
??????}
??????catch?(SqlException)?{
??????????return?false;
??????}
??????finally?{
??????????connection.Close?();
??????}
??}
</script>
Figure 10-12
Forms authentication with role-based security.
Global.asax
<%@?Import?Namespace="System.Data.SqlClient" %>
<%@?Import?Namespace="System.Security.Principal" %>

<script?language="C#" runat="server">
??void?Application_AuthenticateRequest?(Object?sender,?EventArgs?e)
??{
??????HttpApplication?app?=?(HttpApplication)?sender;

??????if?(app.Request.IsAuthenticated?&&
??????????app.User.Identity?is?FormsIdentity)?{
??????????FormsIdentity?identity?=?(FormsIdentity)?app.User.Identity;

??????????//?Find?out?what?role?(if?any)?the?user?belongs?to
??????????string?role?=?GetUserRole?(identity.Name);

??????????//?Create?a?GenericPrincipal?containing?the?role?name
??????????//?and?assign?it?to?the?current?request
??????????if?(role?!=?null)
??????????????app.Context.User?=?new?GenericPrincipal?(identity,
??????????????????new?string[]?{?role?});
??????}
??}

??string?GetUserRole?(string?name)
??{
??????SqlConnection?connection?=?new?SqlConnection
??????????("server=localhost;database=weblogin;uid=sa;pwd=");

??????try?{
??????????connection.Open?();

??????????StringBuilder?builder?=?new?StringBuilder?();
??????????builder.Append?("select?role?from?users " +
????????????? "where?username?=?\'");
??????????builder.Append?(name);
??????????builder.Append?("\'");

??????????SqlCommand?command?=?new?SqlCommand?(builder.ToString?(),
??????????????connection);

??????????object?role?=?command.ExecuteScalar?();

??????????if?(role?is?DBNull)
??????????????return?null;

??????????return?(string)?role;
??????}
??????catch?(SqlException)?{
??????????return?null;
??????}
??????finally?{
??????????connection.Close?();
??????}
??}
</script>
Web.config (Secret Subdirectory)
<configuration>
??<system.web>
????<authorization>
??????<allow?roles="Manager" />
??????<deny?users="*" />
????</authorization>
??</system.web>
</configuration>

As a practical matter, you might prefer to consolidate all your URL authorizations in the top-level Web.config file rather than divide them among Web.config files in individual directories. ASP.NET supports that, too. The following Web.config file, which goes in the application root, enables forms authentication and specifies that only managers are allowed access to resources in the subdirectory named Secret:

<configuration>
??<!--?Configuration?information?for?this?directory?-->
??<system.web>
????<authentication?mode="Forms">
??????<forms?loginUrl="/LoginPage.aspx" />
????</authentication>
??</system.web>
??<!--?Configuration?information?for?the?Secret?directory?-->
??<location?path="Secret">
????<system.web>
??????<authorization>
????????<allow?roles="Manager" />
????????<deny?users="*" />
??????</authorization>
????</system.web>
??</location>
</configuration>

The ability to specify configuration settings for multiple directories in one Web.config file isn’t limited to URL authorizations; it works for other configuration settings, too.

Multiple Roles

It’s not uncommon to encounter organizations in which employees are (or can be) assigned multiple roles. The requestor might be a manager, but he or she could be a developer also or at least want access to material that developers have access to. Does ASP.NET’s brand of role-based security support multiple role memberships? Yes it does. The second parameter passed to GenericPrincipal’s constructor isn’t a string; it’s an array of strings. To indicate that a given security principal (user) belongs to two or more roles, simply submit an array of role names, as shown here:

app.Context.User?=?new?GenericPrincipal?(identity,
????new?string[]?{ "Developer", "Manager" });

Now the requestor can access any resources that managers or developers enjoy access to.

You can also use allow and deny elements to allow or deny access to multiple roles. For example, the statements

<allow?roles="Manager,?Developer" />
<deny?users="*" />

in a Web.config file grant access to developers and managers while denying access to everyone else.

Signing Out

Many sites that rely on forms-style authentication allow users to sign out as well as sign in. Calling any FormsAuthentication method that attaches an authentication cookie to the response effectively signs in the user. The FormsAuthentication.SignOut method does the opposite: it signs out an authenticated user. It works by returning a Set-Cookie header that sets the cookie’s value to a null string and sets the cookie’s expiration date to a date in the past, effectively destroying the authentication cookie. Here’s a snippet of code from a Web form that logs out the current user when the Log Out button is clicked:

<asp:Button?Text="Log?Out" OnClick="OnLogOut" RunAt="server" />
??.
??.
??.
<script?language="C#" runat="server">
??void?OnLogOut?(Object?sender,?EventArgs?e)
??{
??????FormsAuthentication.SignOut?();
??}
</script>

The practical effect is that the next time this user visits a protected portion of your site, he or she will have to log in again.

Authentication Cookie Security

The forms element in Web.config supports the following five attributes:

Attribute

Description

Default

name

Name assigned to authentication cookies

.ASPXAUTH

loginUrl

URL of the login page

login.aspx

protection

Level of protection (validation and encryption) applied to authentication cookies

All

timeout

Lifetime of session authentication tickets in minutes

30

path

Scope of authentication cookies

/

Most of these attributes are self-explanatory, but protection deserves special mention. It specifies the desired level of protection for the authentication cookies that ASP.NET uses to identify authenticated users. The default is “All,” which instructs ASP.NET to both encrypt and validate authentication cookies. Validation works exactly the same for authentication cookies as it does for view state: the machineKey element’s validationKey is appended to the cookie, the resulting value is hashed, and the hash is appended to the cookie. When the cookie is returned in a request, ASP.NET verifies that it wasn’t tampered with by rehashing the cookie and comparing the new hash to the one accompanying the cookie. Encryption works by encrypting the cookie—hash value and all—with machineKey’s decryptionKey attribute.

Validation consumes less CPU time than encryption and prevents tampering. It does not, however, prevent someone from intercepting an authentication cookie and reading its contents. Nonetheless, if you want ASP.NET to validate but not encrypt authentication cookies, set the forms element’s protection attribute as follows:

<forms?...?protection="Validation" />

Encryption provides a double dose of insurance against tampering and prevents the cookie’s contents from being read, too. If you’d like ASP.NET to encrypt authentication cookies but skip the validation procedure, do this:

<forms?...?protection="Encryption" />

Finally, if you want neither validation nor encryption performed, do this:

<forms?...?protection="None" />

The “None” option is useful when authentication cookies travel over HTTPS. After all, there’s no need to encrypt them twice.

Speaking of HTTPS: encrypted cookies can’t be read or altered, but they can be stolen and used illicitly. Time-outs are the only protection a cookie offers against replay attacks, and they apply to session cookies only. The most reliable way to prevent someone from spoofing your site with a stolen authentication cookie is to use an encrypted communications link. If you’d prefer not to encrypt communications to all parts of your site, consider at least submitting user names and passwords over HTTPS. (When you see buttons on commercial sites that say “Sign in using a secure link,” that’s exactly what they’re doing.) The following forms element protects plain-text user names and passwords from prying eyes by connecting to the login form over a secure link:

<forms?...?loginUrl="https://www.wintellect.com/secure/login.aspx" />

This assumes, of course, that your server supports HTTPS and that Login.aspx is stored in a directory configured to use HTTPS.

The path attribute can also play a role in securing authentication cookies. Say you place public files in the virtual root and protected files in a subdirectory configured for HTTPS. If you accept the default path of /, the authentication cookie you acquire is transmitted in all requests to the Web site, not just the ones directed to the Secret directory. An intruder can intercept the cookie on its way to a public page and use it to gain access to protected pages. Here’s the solution:

<forms?path="/Secret" />

Now the cookie will be transmitted only in requests for resources in the Secret subdirectory and its subdirectories, meaning it’s transmitted only over secure channels.

Caveat Emptor

I’ll close this chapter with a word of warning regarding forms authentication—something that’s vitally important to understand but easily overlooked.

Forms authentication protects only ASP.NET files. I’ll say it again: forms authentication protects only ASP.NET files. It guards ASPX files, ASMX files, and other file types registered to ASP.NET, but it doesn’t protect files that don’t belong to ASP.NET—for example, files with .htm or .html extensions. Try it: put a ProtectedPage.html file in the Secret directory used in this chapter’s forms authentication samples. You have to go through the login page to get to ProtectedPage.aspx, but ProtectedPage.html requires no login. That’s because ASP.NET never sees (and therefore can’t intercept and redirect) requests for file types that aren’t registered to it.

One solution is to assign HTML files and other non-ASP.NET files that you want to protect with forms authentication the file name extension .aspx. You’ll incur additional overhead when accessing the files, but at least you won’t leave them alone and unprotected.