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

Ajax Patterns And Best Practices (2006)

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

C H A P T E R 5 P E R M U T A T I O N S P A T T E R N

139

Content-Length: 497

Keep-Alive: timeout=15, max=100

Connection: Keep-Alive

Content-Type: text/html; charset=iso-8859-1

The client receives the HTTP error code 401 and looks for the HTTP header WWW-Authenticate. The value of HTTP WWW-Authenticate contains which authentication mechanism is being requested. In this example, HTTP digest authentication is requested. As a side note, it is possible to use basic authentication, but because it is not considered secure, it is avoided. As a response to the challenge, the browser generates a dialog box similar to Figure 5-11 asking for a username and password. The user types in the username and password, which causes the browser to reissue the original request with the added user authentication information, as shown here:

GET /test/ HTTP/1.1 Host: localhost:8100

User-Agent: Mozilla/5.0 (Macintosh; U; PPC Mac OS X Mach-O; en-US; rv:1.7.8) Gecko/20050511

Accept: text/xml,application/xml,application/xhtml+xml, text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5

Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate

Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Keep-Alive: 300

Connection: keep-alive

Authorization: Digest username="cgross", realm="Private Domain", nonce="0hvlrVH/AwA=8225d4804076a334d81181695204fee405adaaee", uri="/test/", algorithm=MD5, response="fc4ec419438f87a540d8898a537ea401", qop=auth, nc=00000001, cnonce="01b6730aae57c007"

The resulting request is similar to the initial request, except that there is an additional HTTP header, Authorization. When confronted with the same URL request, the server will search for the Authorization HTTP header. If the server finds the header, the server will verify the information and then, depending on the verification, either return another HTTP 401 error causing the browser to generate a dialog box that asks the user to authenticate himself, or consider the user authenticated. If the provided authentication information is correct, the associated representation is downloaded.

When using HTTP authentication, the Authorization HTTP header is sent for all URLs and their dependents that were specified by the WWW-Authenticate header sent by the server. In this example, the value domain="/test" refers to the single URL /test and its dependencies.

Implementing HTTP Authentication

A programmer should not write any code that manages HTTP authentication. All web servers are capable of managing HTTP authentication, and it should be left as an administrative exercise. This does not mean that the programmer does not use HTTP authentication. The programmer still needs to know whether a user is authenticated and needs to associate user identifier information.

140

C H A P T E R 5 P E R M U T A T I O N S P A T T E R N

When developing a server-side application, the approach taken to HTTP authentication is to add the user identifier code to a filter, as illustrated previously in the Accept HTTP header example. The filter will search for the Authorization HTTP header and attempt to create a user identifier. The example used ASP.NET code to extract the Authorization header. Some readers who know ASP.NET code will consider this a wrong approach because ASP.NET has methods and properties that manage authentication. I agree that the ASP.NET methods and properties would be a better solution, but because not all readers will be using ASP.NET, the approach that I took is one that can be applied to all platforms. If there are optimizations, then I say, “Good, use them.” The implementation does not matter anyway because interfaces are used and the applications using the implementations will not care how the user identification information is extracted. In fact, this is why interfaces are used—so that you are not dependent on a particular implementation.

The following source code starts the implementation of the IUserIdentification interface:

public class UserIdentification : IUserIdentification { private string _identifier;

private bool _isIdentified;

public UserIdentification() { _isIdentified = false;

}

public UserIdentification( string identifier) { _identifier = identifier;

_isIdentified = true;

}

public string Identifier { get {

return _identifier;

}

}

public bool IsIdentified { get {

return _isIdentified;

}

}

}

The implementation of UserIdentification has two constructors, with and without parameters. There are two constructors to indicate the two states of user identification: found and not found. The constructor without parameters indicates that no user has been identified. The other constructor, which has a single parameter, indicates that user identification has been found; the single parameter is the state of the identified user. In the implementation of either constructor, the private data member isIdentified is assigned a value of true or false to indicate whether or not, respectively, a user identification has been found.

The properties of UserIdentification define the state of the user identification, and it is important to understand that UserIdentification is a state object. A state object is one where the primary focus is to store data that is used by other processes to make decisions. A state

C H A P T E R 5 P E R M U T A T I O N S P A T T E R N

141

object is very versatile because it does not have other type dependencies and does not manipulate other classes; thus the UserIdentification implementation would be similar for all

IUserIdentificationResolver<> implementations.

The interface IUserIdentificationResolver<> is used to extract the user identifiers. For HTTP authentication, the implementation is illustrated as follows:

public class HttpAuthenticationResolver : IUserIdentificationResolver<HttpRequest> { IUserIdentificationFactory _factory;

public HttpAuthenticationResolver(IUserIdentificationFactory factory) { _factory = factory;

}

public IUserIdentification Resolve(HttpRequest app) { if (request.Headers["Authorization"] != null) {

string identifier = "";

// Do some operations to find out who it is return _factory.Create(identifier);

}

else {

return _factory.Create();

}

}

}

The class HttpAuthenticationResolver implements the interface IUserIdentificationResolver<>, and for the Generics parameter defines the type HttpRequest. What this declaration is saying is that the resolver will extract the user identification information from the type HttpRequest. In ASP.NET, HttpRequest contains all the information that is sent by the request. The constructor for HttpAuthenticationResolver has a parameter, which is an instance of the Factory pattern interface IUserIdentificationFactory. The Factory pattern interface is used by any IUserIdentificationResolver<> implementation whenever an instance of IUserIdentification needs to be instantiated. A Factory pattern implementation is used to instantiate an IUserIdentification instance because the IUserIdentificationResolver<> does not need to know about the type that implements IUserIdentification.

In the implementation of Resolve, the Request.Headers property is referenced to extract the value of the Authorization header. If the HTTP header exists, an identifier is extracted and assigned to the variable identifier, which is passed to the method Create. Using the method Create with parameters indicates that a user has been identified. If the HTTP header is not found, the method Create without parameters is called to instantiate an IUserIdentification instance that indicates that the user has not been identified.

The implementation of Resolve is fairly incomplete and simple because the details are beyond the scope of this discussion; different platforms and environments will be implemented in different techniques. What is complete is the theory of the Resolve method. The theory is that first a check must be made to see whether the HTTP request contains any HTTP authentication headers. If the HTTP headers contain authentication information, the headers must be processed. Regardless of whether or not authentication information is found, Resolve is required to instantiate an IUserIdentification instance.

142

C H A P T E R 5 P E R M U T A T I O N S P A T T E R N

The processing of the HTTP headers cross-references the authorization information with some local information. The local information represents the user identity as defined by the server and could be stored in a database, a Lightweight Directory Access Protocol (LDAP) server, or some other repository. In the example, the variable identifier represents the local information, but the local information does not need to be a single variable, and it could be a structure, class, or some other hierarchy. The form of the local information really depends on the server implementation and nature of the web application. What this results in is a modified version of IUserIdentification, and the factory Create methods. If your local application has a class

to represent the local information, the Create method with a parameter would be modified to pass in a class instead of a simple string buffer. If the local information consisted of two classes, the Create method and IUserIdentification definition would consist of those two classes. The examples proposed are only rules of thumb, but two Create factory methods are needed to indicate an identified user and an unidentified user.

The last step is to wire everything together in the global.asax file. As in the Accept HTTP header example, the user identification code is placed in the BeginRequest handler, which is the first phase called when handling a request. Before the code is shown, let’s ask ourselves whether that is the best place to put the user identification code. Regardless of platform, there are various phases, and one of them is before an authentication phase. As it stands right now, the wiring is happening before the server performs the authentication, which might mean that the authentication by the server is not complete. This in turn might mean that if certain authentication properties and methods are used, they will not be complete. Hence, a better place to wire the user identification routines when using HTTP authentication is after the authentication phase. For ASP.NET, that is the OnAcquireRequestState phase.

Following is the implementation of the method Application_OnAcquireRequestState:

void Application_OnAcquireRequestState(Object sender, EventArgs e) { HttpApplication app = (HttpApplication)sender; IUserIdentificationResolver<HttpApplication> resolver =

new HttpAuthenticationResolver(new UserIdentificationFactory()); IUserIdentification user = resolver.Resolve(app); app.Context.Items["identifier"] = user;

}

In the implementation of Application_OnAcquireRequestState, the object instance sender is typecast to an instance of HttpApplication. The resolver variable references an instance of the HttpAuthenticationResolver type. The implementation of the factory UserIdentificationFactory has not been shown, but is an implementation of the Factory pattern and instantiates the UserIdentification type. Then the method Resolve is called, and an instance of IUserIdentification is returned. These steps can be performed on any platform because they are generic. What is specific to ASP.NET and will be on other platforms is how to hand the user identification information (IUserIdentification instance) to the handler. In the case of ASP.NET, the user identification is assigned to the Context.Items property. On other platforms, it will be some other property that is common to all handlers and filters throughout the life cycle of the HTTP request and response.

As it stands, the server has been wired, and the individual handler needs to reference the user identification whenever content should be accessed or not. To make the HTTP authentication application work, the client has to provide the username and password. Figure 5-11 showed how to send a username and password via the browser, but the following example illustrates how to do the same thing via the XMLHttpRequest object:

C H A P T E R 5 P E R M U T A T I O N S P A T T E R N

143

var xmlhttp = FactoryXMLHttpRequest();

xmlhttp.open( "GET", "/url", true, username, password");

The only change is the addition of the fourth and fifth parameters of the open method. The fourth parameter represents the username, and the fifth parameter is the password. When given those parameters, XMLHttpRequest will use the username and password when XMLHttpRequest is challenged. If there are no authentication challenges, the username and password are ignored. Therefore, when using HTTP authentication and the XMLHttpRequest object, you could always pass the username and password to XMLHttpRequest and let XMLHttpRequest handle the details.

Authenticating When It Is Not Necessary

One of the side effects of HTTP authentication is that content usually is either protected or not protected. Traditionally—and this is why cookies are used—HTTP authentication cannot be off for a resource and then on again for the same resource. That would confuse users because, as it stands right now, HTTP authentication is a global setting and not an individual setting. In other words, if authentication is required for one, then it is required for all. That poses a problem in that if a user wants to browse a site and is purchasing something, that user will need a shopping cart. But to implement a shopping cart, a user identifier is needed. To create a shopping cart, unprotected resources need to be protected. But the protection is global and hence it would mean everybody would need to get a shopping cart after browsing the first page of a shopping site and start buying something. Nice idea to jump-start an economy, but it is not going to happen. To get around this issue of sometimes protection, you can use an HTTP authentication technique.

The technique is as follows:

1.Let the user browse the site as usual (for example, http://mydomain.com/browse).

2.On each browsed page, add a protected link to indicate that the user wants to be authenticated (http://mydomain.com/browse/authenticate).

3.When the user clicks on the authentication link after the authorization, the HTTP realms (domains) that include the nonprotected content are assigned in the response (http://mydomain.com/browse).

4.Then when the user browses the URL http://mydomain.com/browse, user identification information is sent even though it is not required.

This trick works extremely well if you use HTTP digest authentication. Following is an example Apache HTTPD configuration that uses this technique:

<Directory "/var/www/browse/authenticate"> AllowOverride AuthConfig

AuthType Digest

AuthDigestDomain /browse /browse/authenticate AuthDigestFile "/etc/apache2/digestpasswd" AuthName "Private Domain"

Require valid-user </Directory>

144

C H A P T E R 5 P E R M U T A T I O N S P A T T E R N

The technique is implemented by the configuration item AuthDigestDomain, where both the URLs /browse and /browse/authenticate are referenced. Because the configuration item

Directory references the URL /browse/authenticate, only the URL /browse/authenticate will be challenged for an authentication. To illustrate that the technique actually works, consider the following HTTP conversation.

First, a request is made for an unprotected resource:

GET /browse/ HTTP/1.1 Host: jupiter:8100

User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.7.5) Gecko/20041220 K-Meleon/0.9

Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9, text/plain;q=0.8,image/png,*/*;q=0.5

The server responds as usual with an HTTP 200 return code, which causes the client to load the resulting page. Then the client makes another request to the protected link because the user wants to shop and needs to be authenticated. The client makes the following request for the protected content:

GET /browse/authenticate HTTP/1.1 Host: 192.168.1.103:8100

User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.7.5) Gecko/20041220 K-Meleon/0.9

Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9, text/plain;q=0.8,image/png,*/*;q=0.5

The server responds with an authentication challenge:

HTTP/1.1 401 Authorization Required

Date: Sun, 28 Aug 2005 16:08:28 GMT

Server: Apache/2.0.53 (Ubuntu) PHP/4.3.10-10ubuntu4 WWW-Authenticate: Digest realm="Private Domain", nonce="yiLhlmf/AwA=e1bafc57a6151c77e1155729300132415fc8ad0c",

algorithm=MD5, domain="/browse /browse/authenticate", qop="auth"

Content-Length: 503

Content-Type: text/html; charset=iso-8859-1

In the server response for the domain identifier, a nonprotected resource is defined. This is the technique used to send authorization information for nonprotected content. The client responds with user authentication as follows:

GET /browse/authenticate HTTP/1.1 Host: 192.168.1.103:8100

User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.7.5) Gecko/20041220 K-Meleon/0.9

Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9, text/plain;q=0.8,image/png,*/*;q=0.5

Authorization: Digest username="cgross", realm="Private Domain", nonce="yiLhlmf/AwA=e1bafc57a6151c77e1155729300132415fc8ad0c",

C H A P T E R 5 P E R M U T A T I O N S P A T T E R N

145

uri="/browse/authenticate", algorithm=MD5, response="c9b5662c034344a06103ca745eb5ebba", qop=auth, nc=00000001, cnonce="082c875dcb2ca740"

After the authentication, the server allows the downloading of the protected content. Now if the client browses the unprotected URLs again, the authorization information is passed to the server, as illustrated by the following request:

GET /browse/morecontent / HTTP/1.1 Host: jupiter:8100

User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.7.5) Gecko/20041220 K-Meleon/0.9

Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9, text/plain;q=0.8,image/png,*/*;q=0.5

Authorization: Digest username="cgross", realm="Private Domain", nonce="yiLhlmf/AwA=e1bafc57a6151c77e1155729300132415fc8ad0c",

uri="/browse/morecontent/", algorithm=MD5, response="18ccd32175ce7a3480d5fbbc24de8889", qop=auth, nc=00000005, cnonce="0d448aca73b76eb1"

For this request, the client has sent authorization information for a URL that does not require authentication. Simply put, the authentication mechanism has become an “HTTP cookie” mechanism that is controlled by the client. The client is in full control of when to become authenticated and when to remain anonymous.

Using HTTP Cookies

The other way of creating a user identifier is to use an HTTP cookie, as illustrated in Figure 5-9. Frameworks such as ASP.NET have made it very comfortable to implement user identifiers that are cross-referenced with an HTTP cookie. The cross-referencing of the HTTP cookie with the authorization of a resource is not implemented by default in ASP.NET, but it is not difficult to implement.

Generating the Cookie

It is possible to generate an HTTP cookie3 without using any help from a library. Because of the prevalence of cookies, most server-side libraries have classes or functions to generate cookies based on a few parameters. Using the available server-side libraries is highly recommended.

Generating the cookie by using the server-side libraries is not difficult. When using ASP.NET, the following source code would be used:

HttpCookie mycookie = new HttpCookie("Sample", "myvalue"); mycookie.Path = "/ajax/chap05"; Page.Response.Cookies.Add(mycookie);

A cookie is instantiated (HttpCookie) and at a minimum the key (Sample) and value (myvalue) are specified. The combination key-value pair is sent between the client and server. The cookie property mycookie.Path specifies for which URL and its descendents the cookie is valid. Comparing

3. http://www.ietf.org/rfc/rfc2965.txt

146

C H A P T E R 5 P E R M U T A T I O N S P A T T E R N

this to HTTP authentication, the cookie path is equal to the HTTP authentication realm. The newly created cookie is added to the response by using the method Page.Response.Cookies. Add. When a cookie is added, the HTTP response will generate a cookie using the Set-Cookie HTTP header, as illustrated by the following HTTP server response:

HTTP/1.0 200 OK

Server: Mono-XSP Server/1.0.9.0 Unix

X-Powered-By: Mono

Date: Sun, 28 Aug 2005 17:31:14 GMT

Content-Type: text/html; charset=utf-8

Set-Cookie: Sample=myvalue; path=/ajax/chap05

Content-Length: 388

Keep-Alive: timeout=15, max=99

Connection: Keep-Alive

The cookie Sample has a value of myvalue and is valid for the path /ajax/chap05. Because there is no expires value, the cookie is valid only for the lifetime of the browser. If the browser is closed, the cookie is deleted, thus behaving like an HTTP authentication-based user identifier.

Understanding How the Client Manages the Cookie

When the client receives the cookie, the cookie will automatically be saved if the client is a browser or the XMLHttpRequest object of the browser. In fact, the JavaScript on the client side has to do absolutely nothing with the assigned cookie because everything occurs transparently. For example, if a browser loads a page and a cookie is assigned for the entire domain, and then when the XMLHttpRequest object calls a page within the domain, the cookie will be sent.

One thing that is not recommended is the storing of sensitive information within the cookie. Storing passwords or any kind of personal information is not recommended. A cookie is a reference to information, not a repository for information. When a user has been authenticated by using other means, a cookie should be used only as a token to identify the user.

Identifying a User with a Cookie

When the server generates a cookie, it means nothing because a cookie is just a token. Going back to the shopping mall example, it is equivalent to giving each person a token that provides a reference to that person, and as that person wanders the mall, data is generated. To crossreference the token, an authentication mechanism has to be applied. Two authentication mechanisms could be used. The first is to tie the cookie with HTTP authentication. The second is to create an HTML page that associates the cookie with a user.

Using HTTP authentication to associate a user with a cookie would involve protecting a file that requires an explicit authentication. When the user is authenticated by using HTTP authentication the protected file is responsible for associating the cookie and authentication information.

Implementing HTTP authentication in the context of a cookie is similar to the pure HTTP authentication example. The URL used to authenticate the user has a slightly modified implementation. The same interfaces are used in the HTTP authentication example except that the IUserIdentificationResolver<> implementation resolves the authorization and associates it with the cookie. Other than the slight modification of IUserIdentificationResolver<>, the exact same source code as was illustrated in the HTTP authentication can be used. The difference

C H A P T E R 5 P E R M U T A T I O N S P A T T E R N

147

is where the association of user identification to cookie occurs. For the example using ASP.NET, the protected URL would be authentication.aspx, and the implementation of authentication. aspx would be as follows:

<%@ Page Language="C#" %>

<%@ Import Namespace="Component.Authentication" %> <script runat="server">

public void Page_Init( Object source, EventArgs ev) { IUserIdentificationResolver< HttpRequest> resolver =

new HttpAuthenticationToCookieResolver( new UserIdentificationFactory());

IUserIdentification user = resolver.Resolve(Page.Request); if (!user.IsIdentified) {

Page.Response.StatusCode = 500;

Page.Response.StatusDescription = "No authorization information"; Page.Response.SuppressContent = false;

Page.Response.End();

}

else {

Session["useridentifier"] = user;

}

}

</script>

<html>

<head runat="server"> <title>Protected</title>

</head>

<body>

Success!

</body>

</html>

In the ASP.NET page, the function Page_Init implements the initialization phase of the page loading. The init phase is called before the page is processed, and is ideal to test whether the user is authorized. In the implementation, the first two lines, which instantiate the

HttpAuthenticationToCookieResolver type and call the method Resolve, are identical to the user identification examples using HTTP authentication.

What is different from the HTTP authentication examples is that the instantiated IUserIdentification instance is tested to see whether the user is identified. If the user is not identified (!user.IsIdentified), an HTTP 500 error is generated with the message that there is no authorization information. It might be tempting to return an HTTP 401 error to indicate an unauthorized access to the document, but that would be incorrect. It would be incorrect because authentication.aspx is not responsible for implementing HTTP authentication. That is the job of the administrator. If an HTTP 500 error has been generated, what has happened is that the administrator did not protect the URL.

If authorization information is associated with the request, the user variable will reference an authenticated user instance that could be assigned to the Session variable. In ASP.NET,

148

C H A P T E R 5 P E R M U T A T I O N S P A T T E R N

the Session variable is the session information associated with the cookie sent by ASP.NET. Then whenever any handler is called, the Session will contain a reference to the identified user.

The user does not have to be authenticated using HTTP authentication. An HTML form could be used instead. Using the HTML form, the developer is responsible for providing code that manages a user. Because of this added code, the HTTP authentication mechanism is preferred because it is the basis of the HTTP protocol.

Implementing the Shopping Cart

Now that you can authenticate a user, you can associate a shopping cart with a user. When creating a shopping cart, do not consider the shopping cart to be available at one URL. There will be many users of a site, and they will each need their own shopping cart. This means each user gets an individual shopping cart at a unique URL. This is a chicken-and-egg scenario, because if there is an individual URL for a shopping cart, how does the user get that individual URL if they do not know it in the first place? Comparing it to the mall example, it is like saying, “Yeah, we have shopping carts, but they are somewhere in this mall.” Logically all malls put their shopping carts in easy-to-reach locations. This is what has to happen online. The result is the creation of a URL that acts as a directory listing.

If the URL http://mydomain.com/shoppingcart were the easy-to-reach location, calling it would result in the following being generated:

<dir xmlns:xlink="http://www.w3.org/1999/xlink"> <cart

xlink:href="example12345"

xlink:label="unlabelled" xlink:title="Unlabelled Shopping Cart" />

</dir>

The generated result is an XML file that contains a number of contained links defined by using the XML XLink notation. Each generated link represents an available cart. Because each client requires a cart, the generated result does not need to contain all available shopping carts. The generated result needs to contain only one unique available cart. When referencing the shopping cart, the client needs to remember only the links generated in the result.

If the client is operating in anonymous mode, has not been authenticated, and has turned off cookies, the client JavaScript only needs to remember the provided shopping cart link. If the client is authenticated or has allowed cookies, the projected shopping cart links can be associated with the cookie.

Another solution that allows complete anonymity and could be used very effectively is not to save the state on the server side, but on the client side. So whenever the client decides to purchase something, the shopping cart on the client is filled. To check out the items in the cart, the client references a URL and passes the cart to the server. The cart state would be volatile, but it would be a true shopping cart in that no authentication is necessary until the user is ready to check out.

If the shopping cart is based on the generated link, the cart is server side. The shopping cart could be kept for a long time, and implementing the Permutations pattern would allow users to switch devices, browsers, or locations to view their shopping carts. To make the shopping cart work properly, you need to define the Accept and Authorization headers, as illustrated by the following HTTP request: