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

Professional ASP.NET Security - Jeff Ferguson

.pdf
Скачиваний:
28
Добавлен:
24.05.2014
Размер:
13.26 Mб
Скачать

Extending Forms Authentication

If we then visit the private site, we are prompted to log in, as we are anonymous. If our credentials are valid, we see the private site page:

Address].^ htlp7/localhost/security/forms/MultipleAppsPrivate/default-asp;

Welcome to the administration site.

If this application were My developed, you would be able to do all sorts of exciting things. At

present, all we can offer you is f link to the public site

i Done

If we now click the link to the public site, we see the administration options as we are now authenticated:

Welcome to the ecommerce site

You can buy all sorts of exciting things here:

Hats

Gloves

Shoes

Ornaments

Soft Dnnks

This simple example will work whether the two applications are on the same machine, different machines or even machines in totally different domains. Providing the user can navigate to both applications and carry their authentication ticket with them and the <machineKey> elements match, the user will be authenticated by both applications.

NOTE: The security of an application that uses forms authentication depends on the security of the keys held in <machineKey>. Using the same key across multiple applications increases the risk of the keys being stolen. Think very carefully before you use the same keys, particularly on systems with different levels of security.

209

Generating Random Machine Keys

As we have said, it is important for us to generate strong, random keys for us in the <machineKey> element. We will now look at some code that uses a cryptographically strong random number generator to generate key strings suitable for use in <machineKey> elements.

We will create a simple web form that generates two keys of the maximum length. We will then be able to cut and paste them into a web. conf ig file and replicate that file across our web farm.

First, we create some simple presentation code:

<%@ Page language="c#" Codebehind=" default .aspx.cs" AutoEventWireup=" false" Inherits="GenerateMachineKeys ._default" %>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional/ /EN" > <HTML>

<HEAD>

<title>Generate Keys</title>

</HEAD> <body>

<form id="default" method="post" runat=" server ">

<pxasp:Button id="GenerateKeysButton" runat=" server" Text= " Generate Keys " >< /asp : Buttonx/p> <P>

<b>decryptionKey for DES:</b>

<asp : Label id= " DecryptionKeyDESLabel " runat= " server " >< /asp : Label > </p>

<P><STRONG>decryptionKey for Triple DBS (3DES) :</STRONG> <asp:

Label id= " DecryptionKey3DESLabel " runat=" server ">< /asp : Label ></P> <p>

<b>validationKey :</b>

<asp : Label id= " ValidationKeyLabel " runat= " server " >< /asp : Label > </p> </form> </body> </HTML>

We have a button to generate new keys and three labels to display the generated keys (two for the decryption key and one for the validation key).

In the method that handles the click event of the button we make three calls to another method to populate the labels:

private void GenerateKeysButton_Click(object

sender,

System. EventArgs

e) {

 

 

 

 

DecryptionKeyDESLabel .Text

=

CreateMachineKey (16) ;

 

DecryptionKey3DESLabel .Text

=

CreateMachineKey (64) ;

 

ValidationKeyLabel. Text =

CreateMachineKey (128) ;

 

We now need to create the CreateMachineKey method. Here it is:

210

Extending Forms Authentication

public static string CreateMachineKey (int length) { //create a byte array

byte [ ] random = new Byte [length/ 2] ;

//create a cryptographically strong random number generator RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider ( )

//fill the byte array rng.GetBytes (random) ;

//create a StringBuilder to hold the result System. Text . StringBuilder machineKey = new System. Text . StringBuilder ( length) ;

//loop through the random byte array and append to the StringBuilder for (int i = 0; i < random. Length; i++)

{

machineKey . Append ( String . Format ( " { 0 : X2 } " , random [ i ] ) ) ; }

return machineKey. ToStr ing ();

First we create a. byte array to hold our randomly generated data. This is half as long as the requested number of characters as there are two hexadecimal characters to a byte.

byte[] random = new Byte [length/2] ;

Next, we create a new instance of a cryptograpically strong random number generator,

RNGCryptoServiceProvider.

RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider ( ) ;

This class can be found in the System. Security. Cryptography namespace and is a good way to create truly random data. Using this class is much more secure than just calling

System. Random. NextBytes to fill a byte array as RNGCryptoServiceProvider uses the facilities of the windows Crytographic Service Provider, which uses a variety of techniques to ensure that the data it generates is not predictable.

Our next step is to fill the byte array we created with random data from the

RNGCryptoServiceProvider:

rng . GetBy tes ( random) ;

So, now we have a byte array filled with bytes of random data. We need to convert this to a hexadecimal string. To start this process, we create a StringBuilder to hold our string:

System. Text. StringBuilder machineKey = new System. Text. StringBuilder (length) ;

211

Since we know that the final string will contain a certain number of characters, we suggest that length to the StringBuilder so that it can efficiently allocate resources.

We now loop through the bytes in the random byte array:

for (int i = 0; i < random.Length; i++)

Within this loop, we use the String. Format method to convert each byte to two hexadecimal characters (indicated by the X2 in the format string):

machineKey.Append(String.Format("{0:X2}",random[i])); We now have a

web form that will generate the keys with each click of the button:

decryptionKey for DBS: 9EE47A181D2DCDOA

decrypOonKey for Triple DES (3DES):

F134369DEEF3649E28017EEAE564E16C40728E0964128E996C5461DA903AOC4A

validationKey:

52BCDE9AC7A59DB9BCE042FE957DBBBFAB5CFD403BEF5766FD28CE8B5A226F7A72326EA72E076F8844D1166B4591

This is much better approach than manually creating keys for the <machineKey> element.

Forms Authentication Without Cookies

At the start of this chapter, we mentioned that forms authentication is an implementation of the popular method of using cookies to do authentication. It may, therefore, seem strange to talk about doing forms authentication without cookies. The big limitation of standard forms authentication is the requirement that the browser supports and is configured to accept cookies. Many mobile devices do not support cookies. Additionally, it is fairly common for users concerned about their privacy to disable cookies in their browser. If we want to make forms authentication work for these types of user, we will have to find a way to make forms authentication work without cookies. We will have to find another way to persist the authentication ticket between requests.

Fortunately, there is an alternative to cookies for persisting the authentication ticket - the URL. The URL can be used to carry information back to the server in the form of URL parameters. The important thing is that the ticket that we send in the URL is recognized as an authentication ticket and processed by the forms authentication module.

212

Extending Forms Authentication

Thankfully, the developers of ASP.NET have built support for the URL into the forms authentication module. If an authentication cookie is not found in a request, the forms authentication module will check for a URL parameter with the same name as the cookie should have. If we pass the encrypted authentication ticket in this parameter, the forms authentication module will pick it up and use it in exactly the same way as if it had arrived in a cookie.

Note: Web browsers, web servers, and proxy servers place limits on the number of characters in the URL. For example, Internet Explorer 5.5 will only allow 2,048 characters. Bear this in mind if you plan to carry a lot of information in the URL query string.

Let's have a look at how we can recede the simple forms authentication example that we looked at earlier in the chapter so that it is not reliant on cookies.

The presentation code for our login form is exactly the same as before:

<%@ Page language="c#" Codebehind="login.aspx.es" AutoEventWireup="false" Innerits="CookielessAuthentication.login" %>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" > <HTML>

<HEAD>

<title>login</title> </HEAD> <body>

<form id="login" method="post" runat="server"> <p>Username:

<asp:TextBox id="UsernameTextBox" runat="server" /> <asp:RequiredFieldValidator id="UsernameRequiredValidator" runat="server" ErrorMessage="You must enter a username" ControlToValidate="UserNameTextBox">

You must enter your username here </asp:RequiredFieldValidator> </p> <p>Password: <INPUT id="PasswordTextBox" type="password"

runat="server" NAME="PasswordTextBox"> <asp:RequiredFieldValidator id="PasswordRequiredValidator" runat="server" ErrorMessage="You must enter a password" ControlToValidate="PasswordTextbox">

You must enter your password here </asp:RequiredFieldValidator> </p>

<asp:Label id="ErrorMessageLabel" runat="server" Visible="False"> <p>Your username and password did not match. Please try again. </p>

</asp:Label> <asp:Button id="LoginButton" runat="server"

Text="Log In" />   </form> </body> </HTML>

The changes come in the code behind for the login form, specifically in the method that handles the click event of the log in button:

private void LoginButton_Click(object sender, System. EventArgs e) { if { FormsAuthentication . Authenticate (UsernameTextBox . Text ,

PasswordTextBox. Value) ) {

//create an authentication ticket FormsAuthenticationTicket ticket =

new FormsAuthenticationTicket (UsernameTextBox. Text, false, 30);

//encrypt it

string encryptedTicket = FormsAuthentication. Encrypt (ticket) ;

//create a string to hold the URL we will redirect the user to string destinationURL;

//get the original redirection URL string originalURL =

FormsAuthentication. GetRedirectUrl (UsernameTextBox. Text, false) ;

//check whether the original URL has query parameters if (originalURL. IndexOfC1?") == -1)

{

//add the encrypted authentication ticket as the only parameter destinationURL = originalURL + "?" +

FormsAuthentication. FormsCookieName + "=" + encryptedTicket; } else {

//add the encrypted authentication ticket as

//an additional parameter destinationURL = originalURL + "&" +

FormsAuthentication. FormsCookieName + " = " + encryptedTicket;

Response . Redirect (destinationURL) ;

If the user's credentials are successfully verified, we create a new authentication ticket using their username:

FormsAuthenticationTicket ticket = new FormsAuthenticationTicket (UsernameTextBox. Text, false, 30) ;

We then encrypt the ticket by using the FormsAuthentication. Encrypt method to produce an encrypted string that represents the authentication ticket. This is the same string as would be stored in a cookie in the standard scenario.

string encryptedTicket = FormsAuthentication. Encrypt (ticket) ;

214

Extending Forms Authentication

After setting up a string (destinationURL) to hold the URL we will redirect the user to, we get the URL they originally requested by calling the FormsAuthentication.GetRedirectUrl method:

string originalURL= FormsAuthentication.GetRedirectUrl(UsernameTextBox.Text,false);

We now need to check whether the original URL already has a query string, in order that we can add the authentication ticket parameter correctly. We can do this by checking for a "?" in the URL:

if(originalURL.IndexOf("?") == -1)

If the URL has no query string, we must add one by adding a " ? " followed by our authentication ticket parameter:

destinationURL = originalURL + "?" + FormsAuthentication.FormsCookieName + "=" + encryptedTicket;

Note how we use FormsAuthentication.FormsCookieName to get the cookie name that has been configured in the web. conf ig file.

If the URL already has a query string, we must add our parameter to it by adding an "&" followed by our parameter:

destinationURL = originalURL + "&" + FormsAuthentication.FormsCookieName + "=" + encryptedTicket

The final thing we must do is redirect the user to the destination URL that has the encrypted authentication ticket in it.

Response.Redirect(destinationURL);

Once we have set this up, users who are redirected to the login page and whose credentials are validated will be redirected back to their originally requested URL with the encrypted authentication in a query parameter. The forms authentication module will recognize the authentication ticket, decrypt it, and authenticate the user.

Here is a screenshot of the result of our login page doing a redirection:

Addtess £) A591A9A330D 5779011D5CAF5EB9EOF190C3723E6A60B2B184AC2D187852D072DCB4FEODD05EC2W4 TJ f

You can see some of the encrypted authentication ticket in the Address box.

There is still one big problem. If a user is authenticated by an encrypted authentication ticket in the URL and then clicks a link on the page to another restricted page on the site, the URL will not contain the encrypted authentication ticket and the user will be redirected to the login form and forced to enter their credentials again. This is clearly bad for usability.

The solution to this problem is to add the encrypted authentication ticket query parameter to every internal link within the site. This will then ensure that the encrypted authentication ticket is present in the URL of each page the user visits.

We could generate all the links on the site individually, adding code to append the encrypted authentication ticket to them, but this would be time consuming if more than a few links are involved and seems like bad code reuse. A better solution would be to create a custom control for providing links that automatically appends the encrypted authentication ticket when appropriate. That is exactly what we will do.

The HtmlAnchor control provides nearly all the functionality we need - it renders HTML anchor elements that display links to the user. What we need to do is add some code to append the encrypted authentication ticket when it is appropriate to do so.

Here is the full code for the custom control:

using System;

using System.Web.UI;

using System.Web.UI.HtmlControls; using System.ComponentModel; using System.Web.Security;

namespace CookielessAuthentication {

public class AuthTicketAnchor : System.Web.UI.HtmlControls.HtmlAnchor { protected override void Render(HtmlTextWriter output) {

if(Page.Request.QueryString[FormsAuthentication.FormsCookieName] != null) {

if(HRef.IndexOf("?") == -1)

HRef = HRef + "?" + FormsAuthentication.FormsCookieName + "=" + Page.Request.QueryString[FormsAuthentication.FormsCookieName]; else

HRef = HRef + "&" + FormsAuthentication.FormsCookieName + "=" + Page.Request.QueryString[FormsAuthentication.FormsCookieName]; } base.Render(output);

216

Extending Forms Authentication

The first thing to note is that we are not creating a control from scratch - we are deriving our control from the HTML control HtmlAnchor, taking advantage of the functionality it offers. All we need to do is add the additional functionality that we require. It's always worth considering whether we can extend existing controls before we start from scratch.

public class AuthTicketAnchor : System.Web.UI.HtmlControls.HtmlAnchor

Our new code is inside the Render method that is called when the control is requested to output the HTML that displays it. First, we check whether there is an encrypted authentication ticket in the URL:

if(Page.Request.QueryString[FormsAuthentication.FormsCookieName] != null)

If there is, we want to append it to this link. We do this using code very similar to that which we used on our cookieless login page:

if(HRef.IndexOf("?") == -1)

HRef = HRef + "?" + FormsAuthentication.FormsCookieName + "=" + Page.Request.QueryString[FormsAuthentication.FormsCookieName]; else

HRef = HRef + "&" + FormsAuthentication.FormsCookieName + "=" + Page.Request.QueryString[FormsAuthentication.FormsCookieName];

Whether an encrypted authentication ticket was added or not, we must call the render method of the HtmlAnchor class itself in order to output the HTML for the control:

base.Render(output);

Once we have compiled this control into an assembly, we can use it in our pages by adding a register directive at the top of the page, after the Page directive:

<%@ Register TagPrefix="Security" Namespace="CookielessAuthentication" Assembly="CookielessAuthentication" %>

and using the tag in the same way that we might use a server-side <a> tag:

<SECURITY:AUTHTICKETANCHOR id="AuthTicketAnchor1" runat="server" href="anotherPage.aspx">

Go to another restricted page! </SECURITY:AUTHTICKETANCHOR>

So, now we can add links to our pages that will preserve the encrypted authentication ticket in the URL if it is present.

Other potential solutions to the link problem for cookieless forms authentication are:

Q Buffering the output of pages and processing it to correct the links after each page has been generated

G Adding a custom filter to the HttpResponse. Filter property that corrects the links

Protecting Content Other than .aspx Pages

So far, we have only looked at using forms authentication to protect . aspx pages. In Chapter 3 we saw that Windows security, set up in IIS, can be used to protect all the files in a site, application, or folder. By default the same is not true for forms authentication - it will only protect ASP.NET-related files such as . aspx files. The reason for this is that, while every HTTP request passes through IIS, only requests for file types mapped to ASP.NET are forwarded to the ASP.NET ISAPI filter. This means that ASP.NET can only protect files of certain types - those mapped to it in IIS.

If we want to control other file types from within ASP.NET, we have to map those file types to the ASP.NET ISAPI filter.

File types for our application are controlled from the Application Configuration windows in the Internet Information Services Management Console. We get to this by right-clicking our web application (the virtual directory) and selecting Properties, then clicking the Configuration button in the Application section of the Directory tab.

Application Configuration

Mappings j Options j Debugging

|

F

. . .

C:\WINDOWS\Microsoft.NETVrame..

C:\WINDOWS\M icrasofl.NET\Frame.

C:\WINDOWS\MiciosoflNET\Frame.

C:\WINDOWS\Microsoft.NET\Fiame.

C: SWIN D OWS \System32\metsiv\asp.

CAWIN D OWS \M icrosoft. NE T \Frame..

C:\WINDOWS\MioiosoflNETSFrame..

CAWI N DO WS \Systern32SinetsrvSasp.

CAWI ND OWS \System32\inetsrv\asp.

C:SWINDOWSicrosoft.NET\Fjame..

The window lists all of the mappings for our application and the executables that the files of each extension type should be routed to. Note that the ASP.NET-related file types, such as .ASPX or .ASCX, are mapped to an executable with a path something like:

C:\WINDOWS\Microsoft.NET\Framework\vl.0.3705\aspnet_isapi.dll

(This may vary slightly for different versions of the .NET Framework.)

We need to add a new mapping for each file type we want to protect with forms authentication, routing them to the same executable as the ASP.NET file types. The easiest way to do this is to copy and paste from one of the existing mappings (note that, for some reason, keyboard shortcuts for copy and paste don't work in these windows - fortunately, the right-click equivalents work fine).

218

Соседние файлы в предмете Программирование