As mobile browsers become more capable with every release of Android, iOS, and other mobile operating systems, the physical constraints remain — namely the small screen size and the use of touch as the new input paradigm rather than a mouse. While some smartphones might be able to display a website, the display that results is often difficult to use at best, if not unusable altogether. Buttons and other controls need to be sized so that they can be easily manipulated by fingers rather than a mouse pointer, and new actions such as swipes and multi-finger gestures ought to be efficiently leveraged. In short, there needs to be a concerted effort made so that these sites can be not just mobile-friendly, but mobile-enabled.
Reduced screen real estate and touch controls are the primary factors that drive the need to simplify and re-organize the web UI so that a web application becomes usable on a mobile device. While it is possible to produce a single UI that renders differently on desktop and mobile browsers, this approach usually taken to avoid any modification of the existing web application. Instead, the goal should be to produce a set of static assets (markup, stylesheets, and JavaScript™ code) that provide mobile access to web app functionality without the need to modify the server-side code in any way. The existing site remains completely untouched.
From an operational standpoint, this avoids the often costly need to deploy code changes into a production environment. It also eliminates the need to regression test existing functionality that was modified during the course of adding mobile support. Also, because the same requests and responses are used, you only need to performance test the application if you expect adding mobile support to greatly increase your traffic. With this technique, simply drop in your static files (.html, .css, .js) so that they are served from the same domain hosting your existing web application, and reuse the existing services that your application already provides.
By limiting yourself to only creating new static assets, you are in effect only writing a client for your existing web app.
Web applications: A little history
Most non-trivial web applications were built around the notion of Model-View-Controller (MVC), where the controller resided on the server. The server maintained session state and, through forwards or redirects, navigated the user request to the next page to be rendered. Each client request typically resulted in markup for the entire page, even if only parts of that page changed from view to view. Portlet frameworks were introduced to solve this problem and enable more flexibility with page aggregation.
Then, Ajax exploded on the scene and Web 2.0 was born. Ajax enabled asynchronous requests to be made programmatically. The payload no longer had to be markup, but rather XML, and later JSON. This enabled the controller to be moved from server to client, and page aggregation could be offloaded to increasingly more capable and more standardized client browsers. At one extreme, you have a web application where the browser is little more than a markup renderer, with all function and flow managed by the server. On the other end of the spectrum, you have a web application where much of the control and flow logic executes on the client browser (as JavaScript, Flash, or Silverlight), with the server providing RESTful services to supply the data needs and any computational heavy-lifting the application might require.
When mobile web applications are mentioned, it’s this latter pattern that typically comes to mind, because this is how mobile frameworks such as jQuery Mobile or Dojo Mobile have been designed. But what happens when the application you need to mobile-enable has a more server-centric design without REST services?
The web application as black box
Since the intent is to avoid any and all code changes to the application, you can treat the application as a black box and simply focus on the output you get for any given input; input here refers to HTTP requests and output refers to HTTP responses. Therefore, if you create a mobile client that mimics a particular HTTP request and is able to parse and identify all the possible responses for that request, it becomes possible to supply a mobile “skin” that utilizes the same request/response patterns of the existing application. While this sounds simple enough in theory, in practice it might not be quite that easy.
Let’s look at an example of an authentication sequence for a typical web application. (The details about the HTTP request and response can be gathered using a tool such as Firefox Firebug or the Developer Tools in Chrome or Safari.) We'll start at authentication from the point at which the credentials are submitted. Listing 1 shows the source markup of the login form.
Listing 1
<form name="logonForm" action="/abc/salesportal/j_security_check" method="post">
<input type="hidden" name="page" value="signin.wss" />
<table cellspacing="0" cellpadding="0" class="sign-in-table">
<tr>
<td class="c1"><label for="user_id">Email ID:</label></td>
<td><input dir="ltr" class="wpsEditField" size="30"
value="" name="j_username" id="user_id" type="text" />
(e.g., joe@abc.com)</td>
</tr>
<tr>
<td class="c1"><label for="passwd">Password:</label></td>
<td><input dir="ltr" class="wpsEditField" size="30" id="passwd"
name="j_password" type="password" /></td>
</tr>
<tr><td><input type="hidden" name="login-form-type" value="pwd" /></td> </tr>
<tr> <td> </td>
<td><p><a href="http://www.abc.com/password">Forgot your password?
</a></p></td>
</tr>
</table>
<table cellspacing="0" cellpadding="0" class="submit-table">
<tr>
<td class="buttons" align="right">
<span class="button-blue">
<input type="submit" value="Submit" name="submitButton" />
</span>
<span class="button-blue">
<input type="button" value="Cancel" name="cancelButton"
onclick="cancelSignIn()" />
</span>
</td>
</tr>
</table>
</form>
|
In the above code, when the user enters the user ID and password and clicks Submit, you can see that the credentials will be delivered to j_security_check. Simple enough. Let’s simulate an authentication request.
A very useful tool for blackbox web app testing is cURL, which enables you to construct and trace HTTP interactions (see Resources. It gives you full control, enabling you to construct every aspect of the request (Listing 2). Also, you never have to worry about the fickleness of browser caches.
Listing 2
curl --verbose --silent --show-error -d "page=signin.wss" --data-urlencode "j_username=-joe@abc.com" --data-urlencode "j_password=my+passw0rd" --data-urlencode "submit=Submit" http://www.abc.com/abc/salesportal/j_security_check |
Notice that this cURL command ensures all user input fields have been URL encoded with --data-urlencode. This will also automatically set the content-type header to application/x-www-form-urlencoded. Also, the use of the --data* flag implies that this request will be a POST. There are a lot of flags with cURL and you will find you should be able to recreate any request you need.
Unfortunately, as you can see in Listing 3, this simple form POST doesn’t get you the response you expect.
Listing 3
* About to connect() to www.abc.com port 80 (#0) * Trying 192.168.1.1... connected * Connected to www.abc.com (192.168.1.1) port 80 (#0) > POST /abc/salesportal/j_security_check HTTP/1.1 > User-Agent: curl/7.21.3 (i386-pc-win32) libcurl/7.21.3 OpenSSL/0.9.8q zlib/1.2.5 > Host: www.abc.com > Accept: */* > Content-Length: 91 > Content-Type: application/x-www-form-urlencoded > < HTTP/1.1 302 Found < Date: Fri, 18 Nov 2011 16:57:39 GMT < Server: IBM_HTTP_Server/6.1.0.27 Apache/2.0.47 < Location: http://www.abc.com/abc/salesportal/ < Expires: Thu, 01 Dec 1994 16:00:00 GMT < Cache-Control: no-cache="set-cookie, set-cookie2" < Content-Type: www/unknown < Content-Language: en-US < Content-Length: 0 < Proxy-Connection: Keep-Alive < Connection: Keep-Alive < Set-Cookie: JSESSIONID=0001KBumJR-0VZ6ybqAm1WIOn-N:15lgjrufm; Path=/ < Set-Cookie: LtpaToken2=g2VF5mHGQCmv73URFPz...zmsD5ifgdfSiJghQA/jJdUoABkaik=; Path=/; Domain=.abc.com < Set-Cookie: LtpaToken=3UpRmGDYVqd+1ZdKm03v...DSyZhJz4wpt+BFeBXTGn0Ww==; Path=/; Domain=.abc.com < * Connection #0 to host www.abc.com left intact * Closing connection #0 |
You appear to have been authenticated, due to the presence of the LTPA cookies, but your request got redirected to /abc/salesportal/.
By adding the --location flag to your cURL request, the redirects will be automatically followed. On the next attempt, you see the initial POST and 302 redirect (as above) as well as the subsequent request and response from the redirect in Listing 4.
Listing 4
* Connection #0 to host www.abc.com left intact * Issue another request to this URL: 'http://www.abc.com/abc/salesportal/' * Violate RFC 2616/10.3.3 and switch from POST to GET * Re-using existing connection! (#0) with host www.abc.com * Connected to www.abc.com (192.168.1.1) port 80 (#0) > GET /abc/salesportal/ HTTP/1.1 > User-Agent: curl/7.21.3 (i386-pc-win32) libcurl/7.21.3 OpenSSL/0.9.8q zlib/1.2.5 > Host: www.abc.com > Accept: */* > < HTTP/1.1 200 OK < Date: Fri, 18 Nov 2011 16:39:04 GMT < Server: IBM_HTTP_Server/7.0.0.15 < Cache-Control: max-age=300 < Expires: Fri, 18 Nov 2011 16:44:04 GMT < Content-Type: text/html;charset=ISO-8859-1 < Content-Length: 820 < Proxy-Connection: Keep-Alive < Connection: Keep-Alive < Age: 1545 < <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"> <html> <head> <title>Index of /abc/salesportal</title> </head> <body> <h1>Index of /abc/salesportal</h1> <ul><li><a href="/abc/"> Parent Directory</a></li> <li><a href="ccr/"> ccr/</a></li> <li><a href="downloads/"> downloads/</a></li> <li><a href="home2/"> home2/</a></li> <li><a href="html/"> html/</a></li> <li><a href="images/"> images/</a></li> <li><a href="js/"> js/</a></li> <li><a href="media/"> media/</a></li> <li><a href="regs/"> regs/</a></li> <li><a href="style/"> style/</a></li> </ul> </body></html> * Connection #0 to host www.abc.com left intact * Closing connection #0 |
Notice that while you appear to have been successfully authenticated, the resulting page is nothing more than a directory listing. Not what you were expecting. Time to look at the authentication flow in more detail. This time, we'll open up Chrome and use its Developer Tools to view the network details during a successful login (Figure 1).
Figure 1. Chrome Developer Tools

Looking at the network details in Listing 5, you can see that the request posts the credentials, returns some cookies, and then redirects. If you overlook the request headers, you do so at your own peril. Not only is the form data being submitted (and there could be hidden fields), but cookies are as well.
The abcSurvey and UnicaNOIDID cookies look harmless enough, just a survey and site analytics. The JSESSIONID cookie refers to a server side session (Java™) which likely doesn’t contain anything of value that cannot be easily recreated, since you’ve just arrived at the web site and haven’t actually done anything. However, there is one cookie that stands out: WASReqURL.
Listing 5
Request URL: https://www.abc.com/abc/salesportal/j_security_check Request Method:POST Status Code:302 Found Request Headers Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Charset:ISO-8859-1,utf-8;q=0.7,*;q=0.3 Accept-Encoding:gzip,deflate,sdch Accept-Language:en-US,en;q=0.8 Cache-Control:max-age=0 Connection:keep-alive Content-Length:105 Content-Type:application/x-www-form-urlencoded Cookie: abcSurvey=1321469743354; UnicaNIODID=h8DDq7LCdW1-XQbeXFj; JSESSIONID=0001nxuprbDjqGuXSM97y2kwxeq:15bfc0945; WASReqURL=https:///abc/salesportal/signin.wss; Host:www.abc.com Origin: https://www.abc.com Referer: https://www.abc.com/abc/salesportal/registration/signIn.jsp User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.2 + (KHTML, like Gecko) Chrome/15.0.874.106 Safari/535.2 Form Data page:signin.wss j_username:joe@abc.com j_password: my+passw0rd login-form-type:pwd submitButton:Submit Response Headers Cache-Control:no-cache="set-cookie, set-cookie2" Connection:Keep-Alive Content-Language:en-US Content-Length:0 Content-Type:www/unknown Date:Thu, 17 Nov 2011 19:08:17 GMT Expires:Thu, 01 Dec 1994 16:00:00 GMT Location:https://www.abc.com/abc/salesportal/signin.wss Server:IBM_HTTP_Server/6.1.0.37-PM46234 Apache/2.0.47 Set-Cookie: WASReqURL=""; Expires=Thu, 01 Dec 1994 16:00:00 GMT; Path=/ LtpaToken2=jxSSeGottXC2WQZhQFeXf...PP1u+D/8PFtGPQs0sjiYanjdBJEfF0Zj6kXt1QeE=; Path=/ LtpaToken=bTzeMj+SSZFQMUKSZ20tc1...qPSY0kR5MFrKrqZDOwd7twfpupFNUpZUKlEGE41c=; Path=/ |
It appears that the presence of the WASReqURL cookie affects the redirect location.
The response then deletes the WASReqURL cookie by expiring it. It also sends along two new cookies: LtpaToken and LtpaToken2, both of which only get returned upon successful login. The Location header contains the URL to redirect to. That next request follows in Listing 6. Notice how it forwards the newly minted LTPA cookies in the request since WASReqURL has effectively been deleted. The web app now treats this request to /signin.wss as an authenticated request. This second request returns two new cookies: ODC_CG and ODC_REG.
Listing 6
Request URL:https://www.abc.com/abc/salesportal/signin.wss Request Method:GET Status Code:200 OK Request Headers Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Charset:ISO-8859-1,utf-8;q=0.7,*;q=0.3 Accept-Encoding:gzip,deflate,sdch Accept-Language:en-US,en;q=0.8 Cache-Control:max-age=0 Connection:keep-alive Cookie: abcSurvey=1321469743354; UnicaNIODID=h8DDq7LCdW1-XQbeXFj; JSESSIONID= 0001nxuprbDjqGuXSM97y2kwxeq:15bfc0945; LtpaToken2=jxSSeGottXC2WQZh...anjdBJEfF0Zj6kXt1QeE=; LtpaToken=bTzeMj+SSZFQMUKSZ...twfpupFNUpZUKlEGE41c= Host:www.abc.com Referer:https://www.abc.com/abc/salesportal/registration/signIn.jsp User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.106 Safari/535.2 Response Headers Cache-Control:no-cache="set-cookie, set-cookie2" Connection:Keep-Alive Content-Encoding:gzip Content-Language:en-US Content-Type:text/html;charset=ISO-8859-1 Date:Thu, 17 Nov 2011 19:08:20 GMT Expires:Thu, 01 Dec 1994 16:00:00 GMT Server:IBM_HTTP_Server/6.1.0.37-PM46234 Apache/2.0.47 Set-Cookie: ODC_CG=123456; Domain=abc.com ODC_REG=Joe Park|joe@abc.com; Expires=Fri, 16 Nov 2012 19:08:19 GMT; Path=/; Domain=abc.com Transfer-Encoding:chunked |
The payload of this second request includes all the markup to render the authenticated home page for the web app (not shown).
Before you begin writing any code, it’s a good idea to create a standalone request to validate your understanding of the request/response flow. With the insight provided by the network trace in Chrome’s developer tools, you can construct a new cURL login request (Listing 7).
Listing 7
curl --verbose --silent --show-error -d "page=signin.wss" --data-urlencode "j_username=-joe@abc.com" --data-urlencode "j_password=my+passw0rd" --data-urlencode "submit=Submit" --cookie "WASReqURL=http:///abc/salesportal/signin.wss" --cookie-jar login-cookies.txt http://www.abc.com/abc/salesportal/j_security_check |
Notice that the WASReqURL cookie has been added to the request. It is also necessary to have cURL record the cookies (--cookie-jar, see sidebar) since the WASReqURL is expired within a redirect.
cURL output is shown in Listing 8.
Listing 8
* About to connect() to www.abc.com port 80 (#0) * Trying 192.168.1.1... connected * Connected to www.abc.com (192.168.1.1) port 80 (#0) > POST /abc/salesportal/j_security_check HTTP/1.1 > User-Agent: curl/7.21.3 (i386-pc-win32) libcurl/7.21.3 OpenSSL/0.9.8q zlib/1.2.5 > Host: www.abc.com > Cookie: WASReqURL=http:///abc/salesportal/signin.wss > Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 > Content-Length: 91 > Content-Type: application/x-www-form-urlencoded |
The POST request is made with the WASReqURL cookie (Listing 9).
Listing 9
< HTTP/1.1 302 Found < Date: Fri, 18 Nov 2011 19:40:56 GMT < Server: IBM_HTTP_Server/6.1.0.27 Apache/2.0.47 < Location: http://www.abc.com/abc/salesportal/signin.wss < Expires: Thu, 01 Dec 1994 16:00:00 GMT < Cache-Control: no-cache="set-cookie, set-cookie2" < Content-Type: www/unknown < Content-Language: en-US < Content-Length: 0 < Proxy-Connection: Keep-Alive < Connection: Keep-Alive * Added cookie JSESSIONID="0001iuuXHCo5zHq5oTRRZLDYy4B:15lgjrufm" for domain www.abc.com, path /, expire 0 < Set-Cookie: JSESSIONID=0001iuuXHCo5zHq5oTRRZLDYy4B:15lgjrufm; Path=/ * Added cookie WASReqURL="""" for domain www.abc.com, path /, expire 786297600 < Set-Cookie: WASReqURL=""; Expires=Thu, 01 Dec 1994 16:00:00 GMT; Path=/ * Added cookie LtpaToken2="gsHvV95vwKOU8D1CBzy5u1...z6WI0RUTpn9RJLc6OVc4LlFvh2UnanTo0=" for domain abc.com, path /, expire 0 < Set-Cookie: LtpaToken2=gsHvV95vwKOU8D1CBzy5u1...z6WI0RUTpn9RJLc6OVc4LlFvh2UnanTo0=; Path=/; Domain=.abc.com * Added cookie LtpaToken="3UpRmGDYVqd+1Zd788+5x7xr8Km03vwUqWkw4...wpt+BFSht7DYg/ deBXTGn0Ww==" for domain abc.com, path /, expire 0 < Set-Cookie: LtpaToken=3UpRmGDYVqd+1Zd788+5x7xr8Km03vwUqWkw4...wpt+BFSht7DYg /deBXTGn0Ww==; Path=/; Domain=.abc.com |
You get a 302 redirect to /signin.wss. The WASReqURL cookie is expired, and because your credentials are good, you get two LTPA cookies (Listing 10).
Listing 10
* Connection #0 to host www.abc.com left intact * Issue another request to this URL: 'http://www.abc.com/abc/salesportal/signin.wss' * Violate RFC 2616/10.3.3 and switch from POST to GET * Re-using existing connection! (#0) with host www.abc.com * Connected to www.abc.com (192.168.1.1) port 80 (#0) > GET /abc/salesportal/signin.wss HTTP/1.1 > User-Agent: curl/7.21.3 (i386-pc-win32) libcurl/7.21.3 OpenSSL/0.9.8q zlib/1.2.5 > Host: www.abc.com > Cookie: LtpaToken2=gsHvV95vwKOU8D1CBzy5u1...z6WI0RUTpn9RJLc6OVc4LlFvh2UnanTo0=; JSESSIONID=0001iuuXHCo5zHq5oTRRZLDYy4B:15lgjrufm; LtpaToken=3UpRmGDYVqd+1Zd788+ 5x7xr8Km03vwUqWkw4...wpt+BFSht7DYg/deBXTGn0Ww==; WASReqURL=http:///abc/salesportal/ signin.wss > Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 |
The redirect URL (/signin.wss) is followed and the LTPA and JSESSIONID cookies are forwarded. Notice here that cURL erroneously indicates that the WASReqURL cookie is forwarded, despite the fact that it was expired in the previous response. However, because you have enabled cookie persistence in cURL (--cookie-jar), the cookie was not sent, regardless of what this says.
The contents of the cookie-jar file is shown in Listing 11.
Listing 11
# Netscape HTTP Cookie File # http://curl.haxx.se/rfc/cookie_spec.html # This file was generated by libcurl! Edit at your own risk. www.abc.com FALSE / FALSE 0 JSESSIONID 0001iuu...RRZLDYy4B:15lgjrufm www.abc.com FALSE / FALSE 786297600 WASReqURL "" .abc.com TRUE / FALSE 0 LtpaToken2 gsHvV8D1CBzy5...I0RUTpn2UnanTo0= .abc.com TRUE / FALSE 0 LtpaToken 3s1Zd88+5x...hJz4wpt+BXTGn0Ww== .abc.com TRUE /abc/salesportal/ FALSE 0 ODC_CG "" .abc.com TRUE / FALSE 1353181257 ODC_REG Joe Smith|joe@abc.com |
The GET for /signin.wss completes successfully. It returns two more cookies: ODC_CG and ODC_REG (Listing 12).
Listing 12
> HTTP/1.1 200 OK > Date: Fri, 18 Nov 2011 19:40:57 GMT > Server: IBM_HTTP_Server/6.1.0.27 Apache/2.0.47 > Expires: Thu, 01 Dec 1994 16:00:00 GMT > Cache-Control: no-cache="set-cookie, set-cookie2" > Content-Type: text/html;charset=ISO-8859-1 > Content-Language: en-US > Transfer-Encoding: chunked > Proxy-Connection: Keep-Alive > Connection: Keep-Alive * Added cookie ODC_CG="""" for domain abc.com, path /abc/salesportal/, expire 0 > Set-Cookie: ODC_CG=""; Domain=abc.com * Added cookie ODC_REG="Joe Smith|joe@abc.com" for domain abc.com, path /, expire 1353181257 > Set-Cookie: ODC_REG=Joe Smith|joe@abc.com; Expires=Sat, 17 Nov 2012 19:40:57 GMT; Path=/; Domain=abc.com > * Connection #0 to host www.abc.com left intact * Closing connection #0 |
The markup that came back in that final response (not shown) was for the authenticated home page, so you know all your inputs (as well as the proper forwarding of expired cookies) resulted in a successful authentication.
From this entire sequence, you have determined the authentication inputs (Table 1) and, upon successful login, the outputs (Table 2).
Table 1
| Input type | Detail |
|---|---|
| Cookie | abcSurvey=1321469743354; <<< UNIMPORTANT |
| Form | page:signin.wss <<< HIDDEN |
| Form data is encoded as | Content-Type:application/x-www-form-urlencoded |
Table 2
| Output type | Detail |
|---|---|
| Cookies | WASReqURL=https:///abc/salesportal/signin.wss; <<< CREATED
then DESTROYED |
The markup for the authenticated home page is shown in Listing 13.
Listing 13
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html lang="en-US"> <head> <meta http-equiv="content-type" content="text/html; charset=iso-8859-1" /> <meta name="description" content="Registered Home Page Salesportal" /> <meta name="keywords" scheme="iso8601" content="ABC, Salesportal, sellers, home, registered, signed in" /> ... |
So, in order to create a mobile login page, you need to be able to send the same inputs. The form inputs will be no problem, but the cookie, namely WASReqURL, will pose a challenge.
Your mobile login page will be part of a new static file you create — the mobile “skin” mentioned earlier. Let’s call it mobile.html. It will be served from the same server as the existing app, so the Single Origin security Policy (SOP) will not be a hindrance. When the user submits a mobile authentication request, it will be sent by XMLHttpRequest (XHR) which, as you know, is every bit an HTTP request, but done behind the scenes. These XHR or Ajax requests, however, are not identical to your typical HTTP request. Restrictions apply, namely the ability to simply set the “Cookie” header in your programmatic XHR request (as you did with the cURL utility). According to the W3 specification for XMLHttpRequest, this particular header is restricted.
Also, creating the WASReqURL cookie client-side will not cause it to be included in the next response back to the server because the cookie domain can only be set server-side. The only way to include this cookie in a request is to receive it from the server in the first place. Therefore, you need to know when the WASReqURL cookie was created. The request for the login page is shown in Listing 14.
Listing 14
Request URL:https://www.abc.com/abc/salesportal/signin.wss Request Method:GET Status Code: 302 Found Request Headers Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Charset:ISO-8859-1,utf-8;q=0.7,*;q=0.3 Accept-Encoding:gzip,deflate,sdch Accept-Language:en-US,en;q=0.8 Connection:keep-alive Cookie: abcSurvey=1321469743354; UnicaNIODID=h8DDq7LCdW1-XQbeXFj; JSESSIONID= 0001nxuprbDjqGuXSM97y2kwxeq:15bfc0945 Host: www.abc.com Referer: https://www.abc.com/abc/salesportal/home.wss User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.106 Safari/535.2 Response Headers Cache-Control: no-cache="set-cookie, set-cookie2" Connection: Keep-Alive Content-Language: en-US Content-Length: 0 Content-Type: www/unknown Date: Thu, 17 Nov 2011 18:50:02 GMT Expires: Thu, 01 Dec 1994 16:00:00 GMT Location: https://www.abc.com/abc/salesportal/registration/signIn.jsp Server: IBM_HTTP_Server/6.1.0.37-PM46234 Apache/2.0.47 Set-Cookie: WASReqURL=https:///abc/salesportal/signin.wss; Path=/ |
Notice that this initial request for the signin.wss page gets redirected to /registration/signIn.jsp. More importantly, notice that a cookie WASReqURL is sent back in the response. This cookie gets sent along in the second request (Listing 15). (A bug in desktop Safari 5.1.1 for Windows prevents cookies that are created in a 302 response from being forwarded in the subsequent GET request. This works in Safari for iOS however.)
Listing 15
Request URL:https://www.abc.com/abc/salesportal/registration/signIn.jsp Request Method:GET Status Code:200 OK Request Headers Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Charset:ISO-8859-1,utf-8;q=0.7,*;q=0.3 Accept-Encoding:gzip,deflate,sdch Accept-Language:en-US,en;q=0.8 Connection:keep-alive Cookie:abcSurvey=1321469743354; UnicaNIODID=h8DDq7LCdW1-XQbeXFj; JSESSIONID= 0001nxuprbDjqGuXSM97y2kwxeq:15bfc0945; WASReqURL=https:///abc/salesportal/ signin.wss Host:www.abc.com Referer:https://www.abc.com/abc/salesportal/home.wss User-Agent:Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.106 Safari/535.2 Response Headers Connection:Keep-Alive Content-Encoding:gzip Content-Language:en-US Content-Type:text/html;charset=ISO-8859-1 Date:Thu, 17 Nov 2011 18:50:03 GMT Server:IBM_HTTP_Server/6.1.0.37-PM46234 Apache/2.0.47 Transfer-Encoding:chunked |
Recreating the login sequence with Ajax
Based on this investigation, your mobile client will need to make a GET request for /signin.wss for the sole purpose of receiving the WASReqURL cookie before the login form is submitted.
Using jQuery, that request would look like Listing 16.
Listing 16
$.ajax({
type: 'GET',
url: '/abc/salesportal/signin.wss',
async: false,
complete: function(xhr, textStatus) {
if(xhr.readyState == 4)
{
if(xhr.status == 200) { console.log(document.cookie); }
}
}
}); |
In the JavaScript console you should see WASReqURL=http:///abc/salesportal/signin.wss
(among other cookies).
Once the WASReqURL cookie is present on the client, you can create the POST request used to send the login credentials to the server.
Once the user submitted their credentials, you’ll recall the POST to /abc/salesportal/j_security_check in the captured sequence resulted in a 302 redirect to /abc/salesportal/signin.wss. HTTP redirects also result in a method change, in this case from POST to GET. Any cookies returned in the first request will be forwarded in the second request (assuming it is allowed by the cookie Path). All of this occurs transparently within the Ajax request. Programmatically, you will not be able to see the 302 redirect nor the intermediate response and (re)request (Listing 17).
Listing 17
var params = 'j_username=' + encodeURIComponent($('#user_id').val()) + '&j_password='
+ encodeURIComponent($('#passwd').val()) + '&page=signin.wss&submit=Submit';
$.ajax({
type: 'POST',
url:'/abc/salesportal/j_security_check',
contentType: 'application/x-www-form-urlencoded',
data: params,
cache: false,
// Cannot set the Cookie header - restricted
// headers:{'Cookie':'WASReqURL=http:///abc/salesportal/signin.wss'},
// Another attempt to set the Cookie header:
// beforeSend: function(xhr){ xhr.setRequestHeader( "Cookie",
"WASReqURL=http:///abc/salesportal/signin.wss"); },
complete: function(xhr, textStatus) { …}
}); |
Remember to encode user-supplied form-based input using encodeURIComponent() when you are POSTing data with the application/x-www-form-urlencoded content type.
Once you send your authentication request, you need to know whether it was successful or not.
The problem in both cases is that the request will be (transparently) redirected to a new page. With a RESTful approach, you would simply get back a response code of 2xx for success or 401 for unauthorized. Alternatively, you might get back a custom response code packed into a JSON payload. However, for this particular web application, these convenient responses are not available, so we have to make do with the current request-response pattern:
- Successful logins will be redirected to /abc/salesportal/signin.wss
- Unsuccessful logins will be redirected to /abc/salesportal/registration/signInError.jsp
- In both cases the final HTTP status code will be 200 (OK).
The redirect transparency of Ajax requests is problematic because the client will not receive any notification of redirects or what the ultimate URL is (the Location header for the 302 redirect is lost).
The XHR JavaScript object does not make available the request, which means the ultimate request URL is not known. All that is available to determine how the request was handled is to analyse the response headers (on the last request) or the response payload (on the last request).
In the Listing 17, this is done in the complete() function.
To simplify parsing of the response markup, convert the response to XML (if it isn’t already), as shown in Listing 18. (See Resources for a more robust way to accomplish this if you are concerned with more than just WebKit-based browsers.)
Listing 18
complete: function(xhr, textStatus) {
if(xhr.readyState == 4)
{
if(xhr.status == 200)
{
var xmlResponse = xhr.responseXML;
if(xhr.responseXML == null)
{
if(typeof DOMParser != "undefined")
{
var parser = new DOMParser();
xmlResponse = parser.parseFromString(xhr.responseText,
"text/xml");
}
}
}
/*
else if(xhr.status == 302 || xhr.status == 303) {} // will never see this,
only the 200 for the signInError page
else if(xhr.status == 401) // UNAUTHORIZED - will never see this as
application redirects to signInError page
*/
}} |
Once you have the parsed response DOM, the next step is to uniquely identify data on the signin.wss and signInError.jsp pages that is unique to that page. It needs to be always present and, ideally, something that does not change when the page undergoes a UI facelift. Metatags make a prime candidate. The keywords tag is used here because the success page contained “signed in” and the error page contained “failure” and “error” keywords (Listing 19).
Listing 19
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/ xhtml1/DTD/xhtml1-transitional.dtd"> <html lang="en-US"> <head> <meta http-equiv="content-type" content="text/html; charset=iso-8859-1" /> <meta name="description" content="Registered Home Page Salesportal" /> <meta name="keywords" scheme="iso8601" content="ABC, Salesportal, sellers, home, registered, signed in" /> ... |
Using the response DOM retrieved above, jQuery selectors prove to be very useful for extracting the content from the keyword metatag (Listing 20).
Listing 20
var isErrorPage = true;
var metaKeywordsTag = $('meta[name="keywords"]',xmlResponse);
if(metaKeywordsTag != null)
{
var keywords = metaKeywordsTag.attr("content");
if(keywords != null && ((keywords.indexOf("fail") >= 0) || (keywords.indexOf
("error") >= 0)))
{ isErrorPage = true; }
else
{
if(keywords != null && (keywords.indexOf("signed in") >= 0))
isErrorPage = false;
}
} |
Now that you have the isErrorPage flag set, your mobile client can display the next page in the authenticated page flow or the login error dialog.
From the login example described in this article, you can see how it is possible to recreate the same request-response sequence expected by a web application programmatically using XmlHTTPRequest. By treating the application as a black box, faithfully recreating the inputs and using a robust technique to interpret the output, it is possible to avoid making changes to the existing application in order to provide mobile client support. Because there are no server code changes, there is no need to undergo a potentially expensive deployment into a production environment, nor is there a need for extensive regression and performance testing.
Ideally, a website should be architected from the ground up with Responsive Web Design in mind. However, the reality is that there are many websites that were designed without mobile devices in mind, and a significant investment would be required to rebuild them for the mobile web. This technique offers a compromise for getting your website on the mobile web sooner.
Learn
-
Introduction to jQuery Mobile
-
Implement responsive design with jQuery Mobile and CSS3
-
IBM
developerWorks Mobile
-
IBM developerWorks WebSphere
Get products and technologies



