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

Professional ASP.NET Security - Jeff Ferguson

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

Extending Forms Authentication

When we click the Add button in the Mappings tab of Application Configuration, we get the following window:

Add/Edit Application Extension Mapping

Executable:

 

I C: WINDOWSV.Aaspnet_isapi.dll

Browse..

Extension:

 

l-pdf

 

r Verbs .....

 

 

 

j

(• a

 

 

 

Verbs I

C

 

 

Limit to:

 

 

 

 

Script engine Check

 

 

that He exists

OK

Cancel

We add the path of aspnet_isapi . dll and the file extension we want to map in the relevant boxes. We also specify that we want to perform this mapping for all verbs (a verb is a method for requesting the file form the server over HTTP, such as GET or POST).

Once we have done this, requests for files with the relevant extension will be routed to aspnet_isapi .dll and processed by ASP.NET.

Performance Issues

When we map a file type to ASP.NET, requests for that file type have to be dealt with by ASP.NET. This added layer of processing can put extra strain on the web server, adversely affecting performance. It is worth thinking carefully about which file types should be mapped to ASP.NET in this way. For example, mapping the . gi f file type to ASP.NET will mean that every request for a GIF image will have to be processed by ASP.NET. If the application contains a lot of graphics, this could cause a lot of extra processing.

Remember that when we use Windows authentication, the authentication is done by IIS. This means that Windows authentication protects all files served by IIS. This can sometimes make Windows authentication a better choice than forms authentication when we want to protect lots of different file types.

Problems with Some File Types

Problems have been experienced when using forms authentication to protect Adobe Acrobat (. pdf) files. It is possible that similar problems could affect other file types.

The problems are caused by a combination of the ActiveX component that allows Internet Explorer to view Acrobat files, and IIS. The files are sent from the server to the client in chunks so that the user does not have to wait until the whole file has downloaded to start viewing it. For some reason, this system is adversely affected by redirections (for example, using Response.Redirect). Redirecting to a .pdf file causes the file to be reported as corrupted. This causes problems when we try to use forms authentication to protect .pdf files because a redirection back to a .pdf file after a user logs in will result in a corrupted file. The user will be redirected to the login page as normal but after a successful login, the redirection back to the original . pdf file will cause the file to be either reported as corrupted or simply not displayed. If the user enters the URL of the . pdf file after they have logged in and are authorized to view it, the file is displayed correctly; the problem is just with the redirection.

2±9

In order to solve this problem, we have to avoid using FormsAuthentication.RedirectFromLoginPage or Response. Redirect to send the user back to the . pdf file. This might seem like a big problem; how will we get the user back to the file they requested? Fortunately, there is a technique that allows us to get the user back to the .pdf file from the login page. We can add an HTML header to an HTML page that instructs the browser to refresh to another file - our .pdf. This technique does not, for some reason, suffer from the same problem as Response . Redirect.

To do this, we add a header with the name refresh and the content " 0 ;url= [originalUrl] " This will cause the browser to immediately load the target URL (the 0 indicates a delay of 0 seconds).

We can use the Reponse.AppendHeader method to add this header to the response following a successful login, rather than using the redirection methods we have looked at before. Out login code will look something like this:

private void LoginButton_Click(object sender, System. EventArgs e) {

//check the credentials

i f ( FormsAuthentication . Authenticate (UsernameTextBox . Text ,

Pas swordTextBox . Value) ) { //set up the authentication cookie and redirect FormsAuthentication. SetAuthCookie ( UsernameTextBox. Text , false) ;

string url =

FormsAuthentication. GetRedirectUrl (UsernameTextBox. Text , false) ;

Response. AppendHeader( "refresh" , "0;url=" + url); } else

{

//make the error message visible ErrorMessageLabel .Visible = true;

First we set the authentication cookie:

FormsAuthentication. SetAuthCookie ( UsernameTextBox. Text , false) ;

We then get the URL of the originally requested resource:

string url

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

Finally, we add the header that instructs the browser to refresh back to the orginal URL:

Response. AppendHeader ( "refresh" , "0;url=" + url);

Doing this prevents the problems with .pdf files and may prevent problems with other file types. The reason why this works seems unclear; in theory there is little difference between using Response. Redirect and adding the header ourselves.

220

Extending Forms Authentication

Adding Additional Information to the

Authentication Ticket

The primary purpose of the authentication ticket is to allow the forms authentication module to identify the user. However, the authentication ticket also supports the storage of additional information. This is a useful way to persist information for a user without using any server resources - their information is persisted through the encrypted authentication ticket that they carry back to the server with them with each request. The information we persist in this way gains the encryption and validation protection of the authentication ticket.

To add information to the ticket, we will be using the UserData property of FormsAuthenticationTicket. This is a string property that will be encrypted along with the rest of the ticket.

Lets have a look at an example of adding some information to UserData. We will determine a discount value for each user as they log in and persist this in the authentication cookie. We will then be able to use it on each page that they visit. Users will not be able to tamper with the information because of the protection that the ticket has against such activities and we do not have the overhead of persisting this information ourselves. This could be especially useful in a large system that has to deal with large number of users across a web farm - we do not need to worry about sharing session state between servers in the farm (as long as we use the techniques we discussed earlier for implementing forms authentication across a farm).

Here is out new login method:

private void LoginButton_Click(object

sender,

System.EventArgs

e) {

 

 

 

 

 

 

 

 

//check

the

credentials

 

 

 

 

 

if(FormsAuthentication.Authenticate

 

 

 

(UsernameTextBox.Text,

PasswordTextBox.Value))

 

{

 

 

 

 

 

 

 

 

//get

the

user

discount

-hardcoded here

for

this

example int

discount;

 

 

 

 

 

//give preference

to people with

"dan"

in

their

 

name

; - ) if(UsernameTextBox.Text.IndexOf("dan")

!=

-1)

 

 

 

 

 

 

 

 

discount

=

 

 

 

 

 

 

10; else

 

 

 

 

 

 

 

discount

=

5;

 

 

 

 

 

//create a new authentication ticket

FormsAuthenticationTicket ticket = new FormsAuthenticationTicket( 1,

UsernameTextBox.Text,

DateTime.Now,

DateTime.Now.AddHours(3),

false,

discount.ToString());

//encrypt the ticket

string encryptedTicket = FormsAuthentication.Encrypt(ticket);

221

//create a cookie

HttpCookie authenticationCookie = new

HttpCookie (FormsAuthentication.FormsCookieName, encryptedTicket) ;

//write the cookie to the response Response. Cookies .Add (authenticationCookie) ;

//redirect the user back to their original URL Response . Redirect ( FormsAuthentication . GetRedirectUr 1 (UsernameTextBox.Text, false) ) ;

else

//make the error message visible ErrorMessageLabel .Visible = true;

Once the user's credentials have been validated, we determine what the discount should be. In the full application this would probably involve checking a database and some business rules to decide what discount the user should get. Here, we simply check for the username containing " dan " and give the user a 10% discount if it does. If it does not, they get a 5% discount.

//get

the user

discount

-hardcoded here for

this

 

 

example int

discount;

 

 

 

 

//give

preference

to people with

"dan"

in their

 

 

name

;-) if (UsernameTextBox.Text . IndexOf ( "dan" )

!=

 

-1)

 

 

 

 

 

 

 

 

discount

=

10;

 

 

 

 

 

else

 

 

 

 

 

 

 

 

discount

=

5;

 

 

 

 

Next, we create the authentication ticket that we will use:

//create a new authentication ticket

FormsAuthenticationTicket ticket = new FormsAuthenticationTicket ( 1, UsernameTextBox . Text , DateTime .

Now , DateTime . Now . AddHour s ( 3 ) , false, discount . ToString ( ) ) ;

We use the constructor that allows us to specify the UserData in the final parameter. Now that we have a ticket, we have to encrypt it:

string encryptedTicket = FormsAuthentication. Encrypt (ticket) ; We now create an HttpCookie to carry the ticket and add it to the HTTP response:

222

Extending Forms Authentication

HttpCookie authenticationCookie = new

HttpCookie(FormsAuthentication.FormsCookieName,encryptedTicket) ;

//write the cookie to the response Response.Cookies.Add(authenticationCookie);

Finally, we redirect the user back to their original request:

Response.Redirect(FormsAuthentication.GetRedirectUrl

(UsernameTextBox.Text,false));

We now have set an authentication cookie that contains a ticket with our discount. Since we stored this ticket in a cookie with the right name (FormsAuthentication.FormsCookieName), the ticket will be unencrypted by the forms authentication module. We can access it in any request where the user is authenticated, through the Ticket property of the Formsldentity object in User:

if(Request.IsAuthenticated) { DiscountLabel.Text =

((FormsIdentity)User.Identity).Ticket.UserData + "%"; }

else

DiscountLabel.Text = "0% - you are not logged in";

When a user first visits the default page, they will see this:

I our discount is: 0°/o - you are not logged in

Their discount is 0% as they have not logged in yet.

When they use the login page to enter their credentials and are authenticated, they will see this when they visit the default page (if they have "dan" in their username):

1 default

Microsoft Internet Exploiei

 

 

Ffe

Edit

View

Favortes

Tods

 

 

 

 

Help lii

^

 

 

 

 

Search

 

 

If we want to persist more than one value in the authentication ticket, we will need to find a way of encoding the values to keep them separate. We could, perhaps, use a key-value pair system like the one used in the URL query string:

"booksDiscount=10&videoDiscount=5&DVDDiscount=15"

We would have to provide our own code for extracting these values (the String. Split method makes this pretty easy).

The Limits of UserData

The authentication ticket UserData property is not suitable for storing large amounts of data. This is because there is a limit on the size of cookies. This limit is imposed by browsers. If a cookie exceeds the size limit, the browser will simply reject it.

The limit can vary from browser to browser. Internet Explorer and Netscape currently support cookies up to 4,096 bytes in size.

It is important to ensure that we do not write too much information to the authentication ticket - if we do, our cookie will be rejected, and the user will not be authenticated.

If you really have to write more data in an authentication ticket cookie, you can create multiple cookies that store separate pieces of information. To do this, just create the separate authentication tickets and then write them to the response as cookies with different names. The cookie with the name that matches FormsAuthentication.AuthcookieName will be used to authenticate the user and this ticket will be available in Context .User. Identity. Ticket. You will have to decrypt the other tickets yourself by using FormsAuthentication.Decrypt.

Ultimately, cookies are not really designed to persist large amounts of data - they are useful for identification and carrying small data items but for anything larger, we should consider leaving it on the server and linking the user to it through their identity.

Setting Forms Authentication Up to Support Roles-Based Authorization

In Chapter 12 we will be looking at authorization - the process of deciding whether a user has permission to access a resource. One of the key techniques we will cover is 'roles-based authorization', where users are assigned to groups, or roles, that determine what resources they can access. Roles-based authorization uses the IsInRole method required by the IPrincipal interface (see Chapter 6) to determine whether a user is in a particular role. This means that roles-based security will only work when the IsInRole method of the principal we use in Context .User is working properly.

Forms authentication, by default, populates Context.User with a GenericPrincipal. GenericPrincipal has a working IsInRole method but it relies on the roles being specified when the GenericPrincipal is constructed. Standard forms authentication does not specify any roles when a user is authenticated so roles-based authorization will not work with standard forms authentication.

224

Extending Forms Authentication

Roles-based authorization is a flexible and manageable way to assign permissions to users, so it would be nice if we could use it with forms authentication. In order to do so, we need to add our own code to populate the roles in the GenericPrincipal when it is constructed.

As we mentioned earlier, the web. conf ig does not allow us change its schema - we cannot add additional information wherever we might like to. We will therefore use our own XML file to store the users credentials and the roles they belong to. We could use whatever data store we like; providing it will allow us to extract the roles for a user.

We do not want to extract the roles from the data store every time the user is authenticated (that is, every request) so we will use the techniques we covered in the last section to persist each user's roles in the UserData property of their authentication ticket.

Our solution for adding roles to the GenericPricipal will be in three parts:

Q We will add a method to the XMLCredentialsStore class we built in the last chapter, to extract the roles for a user.

Q We will alter the login code to add the roles to the authentication cookie.

Q We will add an event handler to customize the authentication process by populating Context. User with a GenericPrincipal that contains the roles.

The first thing we need to do is provide the means to extract the roles from the XML credentials store. We will do this by adding a method to our XMLCredentialsStore class:

public string[] GetRoles (string username) {

//create an empty array list to hold the roles ArrayList roles = new ArrayList ();

XmlDocument usersXml = new XmlDocument ( ) ; try

{

usersXml .Load (UsersFile) ;

}

catch (Exception error)

{

//we could not load the xml file so we cannot authenticate the user return (stringf] ) roles . ToArray (Type.GetType ( "System. String" ) ) ;

XmlNodeList users = usersXml .GetElementsByTagName ( "user "); f oreach(XmlNode userNode in users)

{

if (userNode. Attributes [ "username" ] .Value == username) {

for each (XmlNode roleNode in userNode. ChildNodes) { if (roleNode. Name == "role")

roles. Add(roleNode. Attributes [ "name" ] .Value) ;

IOC

return (string!])roles.ToArray(Type.GetType("System.String"));

We start by creating an ArrayList to hold the roles that we will extract:

ArrayList roles = new ArrayList();

We then attempt to load the XML document. If anything goes wrong, we convert the empty array list to a string array and return it.

XmlDocument usersXml = new XmlDocument(); try

usersXml.Load(UsersFile);

catch(Exception error)

//we could not load the xml file so we cannot authenticate the user return (string! ])roles.ToArray(Type.GetType("System.String"));

We then get a NodeList of the <user> elements and iterate through them:

XmlNodeList users = usersXml.GetElementsByTagName("user"); foreach(XmlNode userNode in users)

We check whether the username matches the one we are looking for. If it does, we iterate through the elements child nodes, extracting the names of <role> elements and adding them to the array list.

foreach(XmlNode roleNode in userNode. Chi IdNodes) { if (roleNode. Name == "role")

roles .Add (roleNode. Attributes ( "name" ] .Value) ;

Finally, we convert the ArrayList to a string array and return it:

return (string! ] ) roles . ToAr ray ( Type .GetType ( "System. String" ) ) ;

We now have the means to extract roles for a user from our XML credentials store. Our next move is to set up the login form to add the roles to the authentication ticket that is persisted in the authentication cookie. The code we use for this is very similar to that we used in the last section to persist a discount value.

226

Extending Forms Authentication

private void LoginButton_Click(object sender, System. EventArgs e) { //check the credentials

XMLCredentialsStore credentials -

new XMLCredentialsStore (Server . MapPathf "Users .xml" ) ) ;

if (credentials .Authenticate

(UsernameTextBox.Text, PasswordTextBox. Value) ) { //get the roles

string[] roles = credentials .GetRoles (UsernameTextBox.Text) ; StringBuilder rolesString = new StringBuilder ( ) ;

foreach( string role in roles)

{

rolesString. Append(role) ; rolesString. Append (@" ; " ) ;

//create a new authentication ticket

FormsAuthenticationTicket ticket = new FormsAuthenticationTicket ( 1,

UsernameTextBox . Text , DateTime.Now, DateTime.Now.AddHours (3 ) , false,

rolesString. ToString () ) ;

//encrypt the ticket

string encryptedTicket = FormsAuthentication. Encrypt (ticket) ;

//create a cookie

HttpCookie authenticationCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket) ;

//write the cookie to the response Response. Cookies .Add (authenticationCookie) ;

//redirect the user back to their original URL Response . Redirect

(FormsAuthentication.GetRedirectUrl (UsernameTextBox.Text, false) ) ; } else {

//make the error message visible ErrorMessageLabel .Visible = true;

The only real difference here is that we extract the roles from the XML credentials store rather than generating a discount value:

//get the roles

string!) roles = credentials.GetRoles(UsernameTextBox.Text); StringBuilder rolesString = new StringBuilder(); foreach(string role in roles)

{

rolesString.Append(role);

rolesString.Append(@";");

}

We build the authentication ticket using the roles string we have built:

FormsAuthenticationTicket ticket = new FormsAuthenticationTicket( 1, UsernameTextBox.Text,

DateTime.Now,

DateTime.Now.AddHours(3),

false, rolesString.ToStringO ) ;

After that, everything is the same as in the previous example - we encrypt the ticket and persist it in a cookie, and redirect the user back to their original URL.

The last thing we must do to link forms authentication to roles-based authorization is to extract the roles from the cookie when each request is authenticated and populate a GenericPrincipal with the roles. The Context .User. IsInRole method will then work properly and roles-based authorization can come into play.

We need to add some additional code to be executed just after each page is authenticated. Fortunately we can add our own event handler to handle the Application_OnAuthenticateRequest event. Our code will then execute just after the forms authentication module does authentication (and before any authorization happens).

We will handle the event with a method in the global. asax file, so it will apply to all requests:

protected void Application_AuthenticateRequest (Object sender, EventArgs

e){

//check that the request has been authenticated if(Request.IsAuthenticated)

{

//get the roles string[] roles =

((Formsldentity)Context.User.Identity).Ticket.UserData.Split(';');

//create a new principal GenericPrincipal newPrincipal =

new GenericPrincipal(Context.User.Identity, roles);

//add the principal to the context Context.User = newPrincipal;

228

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