Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Asp Net 2.0 Security Membership And Role Management

.pdf
Скачиваний:
48
Добавлен:
17.08.2013
Размер:
12.33 Mб
Скачать

Chapter 5

In the .NET Framework, if you look for a class called “AES” or “Advanced Encryption Standard” you will not find one. Instead, there is a class in the System.Security.Cryptography namespace called RijndaelManaged. Because the AES encryption standard uses the Rijndael encryption algorithm, ASP.NET used the RijndealManaged class when you choose AES.

If an application’s decryptionKey attribute is at the default setting of Autogenerate, IsolateApps, ASP.NET will automatically use the randomly generated 24-byte (192-bit) value that was created for the current process or application identity (Chapter 1 covered how auto-generated keys are stored). This also results in ASP.NET automatically selecting AES as the encryption option.

You can see from this the symmetry in byte sizes for keys between 3DES and AES. In 3DES, the three 56bit keys need to be packaged into three 64-bit values (8 bits in each value are unused as key material by 3DES), which works out to a 192-bit value. The same auto-generated key though can be used with AES because AES supports 192-bit key lengths as well.

If you choose to explicitly specify a value for decryptionKey (and I would highly recommend this because explicit keys are consistent values that you can depend on), then you should ensure that the text value you enter in the <machineKey /> section is one of the following shown in the following table.

Desired AES Key Length in Bits

Number of Hex Characters Required for decryptionKey

 

 

128

32

192

48

256

64

 

 

If you are working on anything other than hobby or personal website always do the following with

<machineKey>:

1.Explicitly set the decryptionKey and validationKey attributes. Avoid using the auto-generated options.

2.Explicitly set the new decryption attribute to the desired encryption algorithm. Choose either 3DES for backward compatibility (more on this in later) or AES.

3.Explicitly set the validation attribute. Choose SHA1, 3DES, or AES (remember that this setting is overloaded for viewstate encryption handling hence the oddity of 3DES or AES specified for a validation algorithm). MD5 is not recommended because it isn’t as strong as SHA1. And of course, just to add to the confusion, choosing SHA1 here really means that forms authentication uses the keyed version: HMACSHA1.

Depending on the auto-generated keys is fraught with peril. For a personal site or a hobbyist site that lives on a single machine, the auto-generated keys are convenient and easy to use. However, any website that needs to run on more than two machines has to use explicit keys because auto-generated keys by definition vary from machine to machine.

There is another subtle reason why you should avoid auto-generated keys. Each time you run aspnet_regiis with the ga option for different user accounts, the next time ASP.NET starts up in a worker process that uses these new credentials, a new set of auto-generated keys is generated! This means if you persistently store any encrypted information (maybe persisted forms authentication tickets for example) that depends on stable values for the key material, you are only one command-line invoca-

202

Forms Authentication

tion of aspnet_regiis away from accidentally changing the key material. Also when you upgrade an ASP.NET 1.1 site to ASP.NET 2.0, the auto-generated keys have all been regenerated with new values. I cover the implications of this in the section about upgrade implications from ASP.NET 1.1 to 2.0.

Generating Keys Programmatically

Encouraging developers to use explicit keys isn’t very useful if there isn’t a way to generate the necessary keys in the first place. Following is a simple console application that outputs the hex representation of a cryptographically strong random key given the number of desired hex characters. If you create similar code on your machine, make sure that the project includes System.Security in the project references.

using System;

using System.Security.Cryptography; using System.Collections.Generic; using System.Text;

namespace GenKeys

{

class Program

{

static void Main(string[] args)

{

if ((args.Length == 0) || (args.Length > 1))

{

Console.WriteLine(“Usage: genkeys numcharacters”); return;

}

int numHexCharacters;

if (!Int32.TryParse(args[0], out numHexCharacters))

{

Console.WriteLine(“Usage: genkeys numcharacters”); return;

}

if ((numHexCharacters % 2) != 0)

{

Console.WriteLine(“The number of characters must be a multiple of 2.”); return;

}

//Two hex characters are needed to represent one byte byte[] keyValue = new byte[numHexCharacters / 2];

//Use the crypto support in the framework to generate the random value RNGCryptoServiceProvider r = new RNGCryptoServiceProvider(); r.GetNonZeroBytes(keyValue);

//Transform the random byte values back into hex characters StringBuilder s = new StringBuilder(numHexCharacters); foreach (byte b in keyValue)

s.Append(b.ToString(“X2”)); Console.WriteLine(“Key value: “ + s.ToString());

}

}

}

203

Chapter 5

After some basic validations, the program determines the number of bytes that are needed based on the requested number of hexadecimal characters: Because it takes two hex characters to represent a single byte value, you simply divide the command line parameter by two. To create the actual random value, call the RNGCryptoServiceProvider class in the System.Security.Cryptography namespace. In this example, I requested that the result not include any byte values of zero.

Converting the byte array back into a hex string is also pretty trivial. The code simply iterates through the byte array of random values, converting each byte into its string equivalent. The “X2” string format indicates that each byte value should be converted to hexadecimal format, and that an extra “0” character should be included where necessary to ensure that each byte is represented by exactly two characters. If you don’t do this, byte values from zero to fifteen require only a single hex character.

The following example of using the tool is generating a 64-character (256-bit) value suitable for use with the AES encryption option.

D:\GenKeys\bin\Debug>genkeys 64

Key value: 7D6E97C7B0685041B5EA562B087C7A6A0718947325E677C10817432020BEA6BF

Setting Cookie-Specific Security Options

Most developers probably use forms authentication in cookie mode. In fact, unless you happened to use the Microsoft Mobile Internet Toolkit (MMIT) in ASP.NET 1.1, ASP.NET could not automatically issue and manage tickets in a cookieless format.

In ASP.NET 1.1 the requireSSL attribute on the <forms /> element enabled developers to require SSL when handling forms authentication tickets carried in a cookie. The slidingExpiration attribute on <forms /> allowed you to enforce whether forms authentication tickets would be automatically renewed as long as a website user stayed active on the site. In addition to these options, ASP.NET 2.0 introduces a new security feature for the forms authentication ticket by always setting the HttpOnly property on the cookie to true.

requireSSL

The HttpCookie class has a property called Secure. When this property is set to true, it includes the string secure in the Set-Cookie command that is sent back to the browser. Browsers that recognize and honor this cookie setting, send the cookie back to the web server only if the connection is secured with SSL. For any high-security site, the requireSSL attrbitue should always be set to true to maximize the likelihood that the cookie is only communicated over a secure connection.

However, depending on client-side behavior is always problematic. The browser may not support secure cookies (unlikely but still possible with older browsers). Additionally, not every user on a website is a person sitting in a chair using a browser. You may have users that are really programs making HTTP calls to your site, in which case it is highly likely that such programs don’t bother looking at or honoring any of the extended cookie settings like the secure attribute. In these cases, it becomes possible for the forms authentication cookie to be sent back to the web server over an insecure connection.

The forms authentication feature protects against this by explicitly checking the state of the connection before it starts processing a forms authentication cookie. If the FormsAuthenticationModule receives a valid cookie (meaning, the cookie decrypts successfully, the signature is valid, and the cookie has not

204

Forms Authentication

expired yet), the module ignores it and clears the cookie from the Request collection if the requireSSL attribute in the <forms /> configuration section was set to true and ASP.NET detects that the connection is not secure. From a user perspective the cookie will not be used to create a FormsIdentity, and as a result no authenticated identity is set on the context’s User property. As a result, the user will be redirected to the login page. Programmatically, the check is easy to do and looks similar to the following:

if (FormsAuthentication.RequireSSL && (!Request.IsSecureConnection))

Both the requireSSL setting and the secured state of the current HTTP connection are available from public APIs.

As a quick example, you can configure an application to use forms authentication but not require an SSL connection, as shown here:

<authentication mode=”Forms”> <forms requireSSL=”false” />

</authentication>

Run the application and login so that a valid forms authentication ticket is issued. Then change the configuration for <forms /> to require SSL:

<forms requireSSL=”true” />

Now when you refresh the page in your browser, you’re redirected to the login page. If you attempt to log in again, the FormsAuthentication class will throw an HttpException when the code attempts to issue a ticket. For example, with code like the following:

FormsAuthentication.RedirectFromLoginPage(“testuser”, false);

you encounter the HttpException if you attempt this when the connection is insecure. Although you would probably think this is unlikely to occur (if you set requireSSL to true in configuration, you probably have SSL on your site), it is possible to run into this behavior when testing or developing an application in an environment that doesn’t have SSL. Because returning unhandled exceptions to the browser is a bad thing, you should defensively code for this scenario with something like the following:

protected void Button1_Click(object sender, EventArgs e)

{

if (FormsAuthentication.RequireSSL && (!Request.IsSecureConnection))

{

lblErrorText.Text = “You can only login over an SSL connection.”; txtPassword.Text = String.Empty;

txtUsername.Text = String.Empty; return;

}

else

{

//Authenticate the credentials here and then...

FormsAuthentication.RedirectFromLoginPage(txtUsername.Text, false);

}

}

205

Chapter 5

The check for the security setting and the current connection security duplicate the similar check that is made internally in a number of places in forms authentication. However, by explicitly checking for this, you avoid the problem of the forms authentication feature throwing any unexpected exceptions. It also gives you the chance to tell the browsers users to use an HTTPS connection to log in. This type of check should be used when calling any forms authentication APIs that may issue cookies such as

RedirectFromLoginPage, and SetAuthCookie.

The requireSSL attribute applies mainly to forms authentication tickets issued in cookies. If an application uses cookieless tickets, or if it has the potential to issue a mixture of cookie-based and cookieless tickets, it is possible to send cookieless tickets over a non-SSL connection. Although ASP.NET still disallows you from issuing cookieless tickets over insecure connections, ASP.NET accepts and processes cookieless tickets received over non-SSL connections. Keep this behavior in mind if you set requireSSL to true and still support cookieless tickets.

HttpOnly Cookies

HttpOnly cookies are a Microsoft-specific security extension for reducing the likelihood of obtaining cookies through client script. In ASP.NET, the System.Web.HttpCookie class adds the HttpOnly property. If you create a cookie and set this property to true, ASP.NET includes the HttpOnly string in the Set-Cookie header returned to the browser. This is a Microsoft-specific extension to the cookie header. I am only aware of it being supported on IE6 SP1 or higher, although there are discussions on the Internet about building in support for it on other browsers. Most other browsers just ignore the HttpOnly option in the cookie header, so setting HttpOnly for a cookie is usually innocuous. However there are some cases of browsers that will drop a cookie with the HttpOnly option (for example, Internet Explorer 5 being one of them). ASP.NET’s cookie writing logic will not emit the HttpOnly option for these cases.

Technically the way HttpOnly cookies work is that if a piece of client-side script attempts to retrieve the cookie, Internet Explorer honors the HttpOnly setting and won’t return a cookie object. In ASP.NET 2.0 the decision was made to enforce HttpOnly cookies all the time for forms authentication. This means that all forms authentication tickets contained in cookies issued by the FormsAuthentication API (for example, RedirectFromLoginPage and SetAuthCookie) will always have the HttpOnly setting appended to them.

There was a fair amount of discussion about this internally because the change has the potential to be a pain for some customers. However, given the fact that many developers are not aware of the HttpOnly option (its original introduction was buried somewhere in IE6 SP1) having a configuration option to change this behavior didn’t seem like a great idea. If few people know about a certain capability, adding a configuration option to turn the capability on doesn’t really do anything to get the word out about it.

Of course, ASP.NET 2.0 could still have added support for HttpOnly cookies by defaulting to turning the behavior on and then exposing a configuration setting to turn it back off again. The counterpoint to this option is that doing so gives developers a really easy way to open themselves up to cross-site scripting attacks that harvest and hijack client-side cookies. The reality is that if developers need a way to grab the forms authentication cookie client-side, the forms authentication APIs can still be pretty easily used to manually create the necessary cookie, but without the HttpOnly option turned on.

Lest folks think that the pain around the decision to enforce HttpOnly for forms authentication tickets is limited to the developer community at large, the ASP.NET team has actually pushed back a number of times when internal groups asked for HttpOnly to be turned off. Repeatedly, theASP.NET team has seen that architectures that depend on retrieving the forms authentication ticket client-side are flawed from a

206

Forms Authentication

security perspective. If you really need the forms authentication ticket to be available from a client application, using the browser’s cookie cache as a surrogate storage mechanism is a bad idea. In fact, scenarios that require passing a forms authentication ticket around on the client-side frequently also depend on the need for persistent tickets (if the ticket were session-based, there would be no guarantee that the cookie would still be around for some other client application). So, now you start going down the road of persistent cookies that are retrievable with a few lines of basic JavaScript, which isn’t a big deal for low security sites, but definitely something to avoid in any site that cares about security.

To see how the new behavior affects forms authentication in ASP.NET 2.0, you can write client-side JavaScript like the sample shown here.

<html>

<head><title>You were logged in!</title></head> <body>

<script language=javascript> function ShowAllCookies()

{

var c = document.cookie; alert(c);

}

</script>

<form id=”form1” >

<input type=button onclick=”ShowAllCookies();” value=”Click to see cookies.” /> </form>

</body>

</html>

If you run this code on an ASP.NET 1.1 site that requires forms authentication, you get a dialog box that conveniently displays your credentials such as the one shown in Figure 5-1:

Figure 5-1

If you run same client-side script in an ASP.NET 2.0 application after logging in, you won’t get anything back. Figure 5-2 shows the results on ASP.NET 2.0:

Figure 5-2

207

Chapter 5

As mentioned earlier, if you really need client-side access to the forms authentication cookie, you need to manually issue the cookie and to manage reissuance of the authentication cookie in case you want to support sliding expirations. (With sliding expirations, FormsAuthenticationModule may reissue the cookie on your behalf.)

Although HttpOnly cookies make it much harder to obtain cookies through a client-side attack, it is still possible to trick a web server into sending back a page (including cookies) in a way that bypasses the protections within Internet Explorer. There are a number of discussions on the Internet about using the TRACE/TRACK command to carry out what is called a cross-site tracing attack. In essence, these commands tell a web server to send a dump of a web request back to the browser, and with sufficient client-side code, you can parse this information and extract the forms authentication cookie. Luckily, this loophole can be closed by explicitly disabling the TRACE/TRACK command on your web servers and/or firewalls.

slidingExpiration

You may not think of the sliding expiration feature as much of a security feature, but this setting does have a large effect on the length of time that a forms authentication cookie is considered valid. By default in ASP.NET 2.0 sliding expiration is enabled (the slidingExpiration attribute is set to true in <forms />). As long a website user sends a valid forms authentication cookie back to the web server before the ticket expires (30-minute expiration by default), the FormsAuthenticationModule periodically refreshes the expiration date of the cookie. The FormsAuthentication.RenewTicketIfOld method is used to create an updated ticket if more than 50 percent of the ticket’s lifetime has elapsed.

The security issue is that with sliding expirations a website user could potentially remain logged on to a site forever. Even with the 30 minute default, as long as something or someone sends a valid ticket back to the server every 29 minutes and 59 seconds, the ticket will continue to be valid. On private computers or computers that are not in public areas, this really isn’t an issue. However, for computers in public areas like kiosks or public libraries, if a user logs into a site and doesn’t logout, the potential exists for anyone to come along and reuse the original login session.

You can’t control the behavior of your customers. (Even with a logout button on a website, only a small percentage of users actually use it.) You do, however, have the option to disable sliding expirations. When slidingExpiration is set to false, regardless of how active a user is on the website, when the expiration interval passes the forms authentication ticket is considered invalid and the website user is forced to log in again. Of course, this leads to the problem of determining an appropriate value for the timeout attribute. Setting this to an excessively low interval annoys users, whereas setting it to a long interval leaves a larger window of opportunity for someone’s forms authentication ticket to be reused.

Using Cookieless Forms Authentication

ASP.NET 2.0 introduces automatic support for issuing and managing forms authentication tickets in a cookieless manner. In Chapter 1, you learned that earlier versions of ASP.NET had a mechanism for managing the session state identifier in a cookieless manner. ASP.NET 2.0 piggybacks on this mechanism to support cookieless representations of forms authentication tickets, as well as anonymous identifiers (this second piece of information is only used with the Profile feature). You can enable cookieless forms authentication simply by setting the new cookieless attribute to in the <forms /> configuration section:

<forms ... cookieless=”UseUri” />

208

Forms Authentication

The following table lists the options for the cookielessattribute.

Cookieless Attribute Value

Descrption

 

 

UseUri

Always issues the forms authentication ticket so

 

that it shows up as part of the URL. Cookies are

 

never issued.

UseCookies

Always issues the forms authentication ticket in a

 

cookie.

AutoDetect

Detects whether the browser supports cookies

 

through various heuristics. If the browser does

 

appear to support cookies, issues the ticket on the

 

URL instead.

UseDeviceProfile

Finds a device profile for the current browser

 

agent, and based upon the information in the

 

profile, uses cookies if the profile indicates they are

 

supported. This is the default setting in ASP.NET

 

2.0. Information for the device profiles is stored in

 

the Browsers subdirectory of the framework’s CON-

 

FIG directory. ASP.NET ships with a set of browser

 

information, including cookie support, for widely

 

used browsers. You can edit the files in this direc-

 

tory, or add additional setting files, and then make

 

the changes take effect with the aspnet_reg-

 

browsers.exe tool.

 

 

The default setting for the cookieless attribute is UseDeviceProfile. This means that your site will issue a mixture of cookie-based and URL-based forms authentication tickets, depending on the type of browser agent accessing your website. If you don’t want to deal with some of the edge cases that occur when using cookieless tickets, you should set the cookieless attribute to UseCookies.

The nice thing about cookieless support in ASP.NET 2.0 is that other than changing a single configuration attribute, forms authentication continues to work. As a very basic example, issuing a cookieless forms authentication ticket on a login page with the familiar FormsAuthentication.RedirectFromLoginPage method results in a URL that looks something like the following (the URL is wrapped because the cookieless representation bloats the URL size):

http://localhost/Chapter5/cookieless/(F(fEyM7SWsyey0thapoZubKAefgscwcjg_ycZgHjS9kPF 1Z0FduNGYQARyDiB4e5UmfSm6llaQ9o-5hUpLVdx4oIYrqg8vecM15Yvi-bD3Xb41))/Default.aspx

The bold portion of the URL is, of course, the forms authentication ticket. Behind the scenes, as was described in Chapter 1, aspnet_filter.dll manages the hoisting of the cookieless values out of the URL and converting it into a custom HTTP header. Internally, cookieless features such as forms authentication rely on internal helper classes to move data from the custom HTTP header into feature specific classes, such as FormsAuthenticationTicket. If you dump the HTTP headers for the page in the previous URL, you will see the end result of the work performed by aspnet_filter.dll:

HTTP_ASPFILTERSESSIONID=F(fEyM7SWsyey0thapoZubKAefgscwcjg_ycZgHjS9kPF1Z0FduNGYQARyD iB4e5UmfSm6llaQ9o-5hUpLVdx4oIYrqg8vecM15Yvi-bD3Xb41)

209

Chapter 5

Unfortunately, in ASP.NET 2.0, the general-purpose class used internally for parsing the cookieless headers is not available as a public API. So, unlike the HttpCookie class, which gives developers the flexibility to create their own custom cookie-based mechanisms, cookieless data in ASP.NET 2.0 is supported only for the few features like forms authentication that have baked the support into their APIs.

Cookieless Options

You have seen the various cookie options that you can set on the cookieless attributes. Of the four options, UseCookies and UseUri are self-explanatory. However, I want to drill in a bit more on the other two options: AutoDetect and UseDeviceProfile.

AutoDetect

The AutoDetect option comes into play when forms authentication needs to determine whether a forms authentication ticket should be placed on the URL. ASP.NET 2.0 will go through several checks to see whether the browser supports cookies. Although going through this evaluation means that the initial ticket issuance takes a little longer, it does mean that for each and every new user on your website, you have a very high likelihood of being able to issue the forms authentication ticket in a way that can be received by the user’s browser. If new browsers are introduced, and the device profile information is not available yet on your server (an extremely common case in the mobile world where there seems to be a new device/browser/. . . every day), the AutoDetect option is very handy.

When a browser first accesses a site, it is requesting one of three possible types of pages:

Pages that allow anonymous users and, thus, do not require authentication.

The forms authentication login page for the site.

A secured page that requires some type of authenticated user. In this case, authorization will eventually fail and force a redirect back to the login page.

Phase 1 of Auto-Detection

In the first case, forms authentication lies dormant and the auto-detect setting has no effect. After a browser accesses the types of pages indicated by the second and third bullet points, the FormsAuthenticationModule starts the process to detect whether or not the browser supports cookies. Depending on whether the browser is accessing the login page or a secured page, the internal path leading to auto-detection is a bit different. However, from a functionality perspective the browser experiences the same behavior.

The detection process goes through the following steps in sequence:

1.A check is made using the browser capabilities object available from Request.Browser. The information returned by this object is based on an extensive set of browser profiles stored on disk in the Browsers directory. If the browser capabilities definitively indicate that cookies are not supported, there is no additional detection needed. Short-circuiting the auto-detection process at this point saves time and unnecessary redirects. For classes of devices that simply do not support cookies, there isn’t any point in probing further in an attempt to send cookies.

2.If the browser capabilities for the current request indicate that cookies are supported, then a check is made to see if auto-detection occurred previously. If a previous browse path through the site already occurred, and if the results of that browsing indicated that cookies weren’t sup-

210

Forms Authentication

ported, the URL will already contain extra information indicating that this check occurred. Normally though, a user browses to the login page or a secured page for the first time, and thus auto-detection will not already have occurred.

3.A check is made to see if cookies have been sent with the request. For example, your site may have already issued some other kind of cookies previously when the user was browsing around. In this case, the mere presence of cookies sent back to the server is an indication that cookies are supported.

4.If all of the previous checks fail, ASP.NET adds some information to the current response. It adds a cookie to Response.Cookies called “AspxAutoDetectCookieSupport.” It also appends a query-string name-value pair to the current request path — the query-string variable is also called “AspxAutoDetectCookieSupport.” Because the only way to get this query-string variable onto the path in a way that the browser can replay it, a redirect back to the currently requested page is then issued.

The net result of this initial detection process is that for the nominal case of a browser first accessing the login page, or a secured page, a redirect to the login page always occurs. In the case that the user was attempting to directly access a secured page, the extra query-string and cookie information is just piggybacked onto the redirect that normally occurs anyway. On the other hand, if the user navigated to the login page directly, then ASP.NET forces a redirect back to the login page in order to set the query-string variable. In the browser’s address bar, the end result looks something like the following:

http://demotest/chapter5/cookieless/login.aspx?AspxAutoDetectCookieSupport=1

At this point if the browser supports cookies, there is also a session cookie held in the browser’s cookie cache called “AspxAutoDetectCookieSupport.” So, there is potentially both a query-string variable and a cookie value client-side in the browser waiting to be sent back to the web server. Of course, on browsers that don’t support cookies, only they query-string variable will exist.

Phase 2 of Auto-Detection

After the user types in credentials and submits that login page back to the server, the auto-detect steps listed earlier are evaluated again because the FormsAuthenticationModule always triggers these steps for the login page. However, because the auto-detection process already started, one of two decisions is made:

If the browsers supports cookies, then the auto-detect cookie will exist and the forms authentication feature will determine that cookies are supported.

If the auto-detect cookie was not sent back by the browser, then a check is made for the autodetect query-string variable. Because this query-string variable now exists, ASP.NET will add a cookieless value to the URL that indicates the browser does not support cookies. A value of “X(1)” is inserted into the URL and will exist in all subsequent requests that the browser makes to the site for the duration of the browser session.

Phase 3 of Auto Detection

The code in the login page needs to process the credentials that were posted back to it at this point. If the credentials are invalid, then the browser remains on the login page, and Phase 2 will repeat itself when the user attempts another login. If the credentials are valid though, then usually either FormsAuthentication

.RedirectFromLoginPage or FormsAuthentication.SetAuthCookie is called to create the forms authentication ticket and package it up to send back to the client.

211