Introduction
In the first part of this article, we've discussed Service Oriented HTML Application's key distinguish features, it has loosely coupled front end and service, backend service is presentation agnostic business logics and provides data access, web server serves static HTML/CSS/JavaScript/Assets files without any server pages generating HTML, and on the client side, DOM is built out based on Ajax data, creating rich interactivities without the dependency on browser plugin.
We also discussed some demo code and demo page that shows the power of jQuery to creating and changing different user preferences and layout/themes without the help of server page (ASP, ASP.NET MVC, JSP, PHP, Ruby on Rails, ColdFusion, etc.), it dynamically loads HTML, CSS and JavaScript at runtime on demand. These are the fundamentals for building SOHA - Service Oriented HTML Application - all by static files on the web server.
Since there is no role reserved for server page technologies and client browser plugin in SOHA, beyond page dynamics discussed in part 1, we're facing some challenges in this architecture, like: session management, security, navigation, page to page data transfer and client/service interactions by Ajax, etc. This part provides some details on the solutions that resolved those challenges.
SOHA Session Management
To some degree, SOHA is about building a Rich Internet Application without browser plugin, i.e., no dependency on Flash Player, Silverilght and JavaFX. Regular RIA web site is built entirely on Flex or Silverlight, it doesn't have any server pages either, front end relies on data coming from services. This service oriented aspect remains the same when we replace the code in ActionScript/C#/Java with HTML/CSS/JavaScript, SOHA session management is essentially the same as regular RIA.
Just as RIA does, there are various techniques to cope with session management in SOHA, the approach I prefer and also tested, is that to have service own the responsibility of session management and user authentication, client logic will take action based on service response.
Since service is usually designed in RESTful style and accessed via HTTP(s), it makes sense for service using cookie as session key once session is established on service. For consecutive service calls by Ajax, the browser makes sure to attach the valid cookies to the request. Before app server process the request, it normally runs through a "security filter" to validate the cookie. If the cookie is invalid or not associated with any session, it would return HTTP 401 (Not Authorized), and then client side Ajax code can take appropriate action to navigate to public zone or notify the user that login is required. If the cookie is valid and associated with a live session, authentication is completed; control will pass down to the requested service method.
During the process, the cookie is set by service and consumed by service, client code never touches them, and service has the full control of the cookie life cycle. Service usually sets them after user login and resets cookies for each consecutive authenticated calls. In secure web app, cookies are normally session cookie, it expires when user logs out, shuts down the browser or when session time out. One point worth noting is that client side logic only responses to the service response, it does not care about cookie values. The only time that client needs to deal with cookie is when application loads, it helps service to detect whether cookie is enabled, since service relies on cookies as session key.
Usually, service has a 20 minutes session and needs coordination with client to provide better user experience for managing session time. In order to avoid HTTP 401 (Not Authorized) error when user sitting idle for too long, we need a client side session as well. Here below is how the client side session works in order to achieve a user friendly session management:
- Client side session timer starts after user logs in, and resets itself whenever a secure Ajax call returns (since service resets it's timer too);
- Client side session times out before service time out. For example, when service times out after 20 minutes, client side timer would time out in 18 minutes;
- When it times out before service session ends, it prompts the user their session is about to expire, gives the options to extend the session or logout, then a secondary client side timers starts, it would time out in 90 seconds;
- Within the 90 seconds, if user selects to extend the session, client initiate a call to service, say
KeepAlive
, to give both the service and client a chance to reset their session timer; - After 90 seconds, the secondary timer times out, it means user ignored the "about to expire" prompt, then client would initiate a
logout
service call on behave of user, and navigate to application's public zone programmatically.
The following is some pseudo code for login and start client side session:
function onBeforeLoginSubmit()
{
if ($('#sohaLoginForm').valid())
{
var loginObj = {};
$("#headerLoginForm").find("input").each(
function () {
loginObj[(this).name] = (this).value;
}
);
$.cbexp.beginWaitFormSubmit("sohaLoginForm");
$.cbexp.postJson(AUTH_SERVICE_PUBLIC_URL +
"/login", loginObj, onLoginResult, onLoginError);
}
return false;
}
function onLoginResult(response)
{
if (response == null || response.status.code != 200)
{
$.cbexp.sohaNavigate("login.html?mode=error");
}
else
{
$.cbexp.setSOHASecureToken(response.sohaSecureToken);
$.cbexp.navigateWithToken("home.html");
}
$.cbexp.endWaitFormSubmit("sohaLoginForm");
}
function onLoginError(xhr, ajaxOptions, thrownError)
{
$.sohaError.showServiceError("sohaLoginForm",null, xhr.status);
$.cbexp.endWaitFormSubmit("sohaLoginForm");
}
When home.html loads, it starts client side timer, logic stays true
for all protected HTML files:
function startSecurePageProcessing()
{
if ($.idleTimeout == undefined)
return;
$.idleTimeout('#idletimeout', '#idletimeout a', {
idleAfter: $.cbexp.idleAfter,
warningLength: $.cbexp.warningLength,
onTimeout: function ()
{
$(this).slideUp();
$.cbexp.logout(null, function () {
$.cbexp.sohaNavigate("login.html?mode=timeout");
});
},
onIdle: function ()
{
$(this).slideDown();
},
onCountdown: function (counter)
{
$(this).find("span").html(counter);
},
onResume: function ()
{
$(this).slideUp();
$.cbexp.postJson(SESSION_SERVICE_SECURE_URL +
'/keepalive', $.cbexp.getNavTokenPostData())
}
});
}
This approach makes sure session time is managed in a user friendly and secure manner. But with SOHA, since different page is built with static XHTML template, when we navigate programmatically from login.html (public zone page) to home.html (protected zone page), the DOM is torn down for login.html and rebuilt for home.html, all authentication information available to home.html is the session cookie, but client code doesn’t care about cookie value, how can the home.html assume user is already logged in?
There are two special use cases for browser to load a secure page: one is user book marked home.html URL, logs out or shuts down browser, then they try to access the bookmark page directly, the protected page like home.html cannot assume user is already logged in. Another case is one logged in one browser's tab, then opens a new browser tab (same browser though) and logged in as different user, when they switch back to tab one, the protected XHTML page should not assume user is already authenticated.
With SOHA, any protected page, like home.html, can never assume user authenticated when it loads. This is the navigation security part of SOHA session management; we'll discuss it in detail in the next section.
SOHA Navigation Security
To ensure authenticated user navigates different pages securely in SOHA is a unique architecture challenge, since a SOHA is built upon a series static
XHTML template files, there is no server page's processing pipeline to help validate a static
resource (HTML file in this case) request is authenticated and authorized. As a matter of fact, when browser requests a protected HTML page in SOHA, the request has never been validated, remember the web server is just Apache with no server side logic running (more in part 1)? The web server always serves down the requested HTML file without any constrains. It's up to the SOHA Controller, the scripts referenced by each protected page, to make sure user is authenticated and authorized whenever the page loads.
The basic idea for SOHA navigation security is that whenever a protected page loads, it should assume user has not logged in, a site-wide shared security scripts need to run to check on authentication by calling service (since service sets and validates the session cookie). If service recognized the authentication information sent from client, it authorizes access, otherwise redirects the user to a public zone page for user credentials.
The “Not Authenticated” assumption stays the same no matter how a protected page loads, including hyperlinks navigation, programmatic navigation, loads from browser history, and access via book marked. This assumption demands the security script to run every time a new protected page loads, we so we need to disable the browser cache for all protected XHTML file.
Disable Browser Cache for Protected Pages
In order to make sure the security scripts runs every time the page loads, including navigated programmatically, user typed in URL, book marked URL or refresh, browser cache needs to be disabled for all protected pages. It can be done by setting the following meta tag in HTML files:
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
<meta http-equiv="Expires" content="0">
The simple approach above works great in Internet Explorer, Firefox, Chrome, but doesn't work as expected in Safari on Mac. We use the following trick to work around it, it's a pure client side technique, no server page involved: it simply registers a page unload event handler, when Safari sees the event handler, it stops loading the HTML from local cache:
$(window).bind('unload', $.cbexp.unloadingPage);
Run security check when page loads
Here below is the pseudo code of security check, runs every time the protected page loads, it asks back end service for authentication information, then authorizes or denies the access to the protected page:
$(function ()
{
if (typeof (preInitPage) == 'function')
preInitPage();
var sohaSecureToken = $.cbexp.getSOHASecureToken();
if (null == sohaSecureToken || undefined == sohaSecureToken)
{
denyAccessThenRedirect("This is a secure page, please login first.");
}
else
{
$.cbexp.postJson(SESSION_SERVICE_SECURE_URL +
'/authenticate', sohaSecureToken, onAuthenticateResult, onAuthenticateError);
}
});
function denyAccessThenRedirect(errorMessage)
{
$.cbexp.prepareRedirect();
alert(errorMessage);
$.cbexp.redirectNewUser();
}
function onAuthenticateResult(response, textStatus, XMLHTTPResponse)
{
if (response.status.code != 200)
{
denyAccessThenRedirect("Your session is invalid,
please login first. (Status Code: " + response.status.code + ")");
}
else
{
$('#pageFooter').load('shared/_footer.html');
$('#pageHeader').load('shared/_header.html', null,
function() {
$.cbexp.initSecurePageHeader();
startSecurePageProcessing();
});
}
}
function onAuthenticateError(xhr, ajaxOptions, thrownError)
{
$.cbexp.showServiceError("authenticate", null, xhr.status);
denyAccessThenRedirect("Your session can not be validated,
please login first.(Status Code: " + xhr.status + ")");
}
If we revisit the use case one, when user tries to access a book marked protected page, the above onDocumentReady
event handler will call authenticate
service, if user is in a valid session (access the bookmark after login), service would response with status code 200, means authenticated, then access is authorized, shared header/footer get loaded and initialized, then page specific process method, startSecurePageProcessing
, is executed. The logic stays the same when user refreshes the page within a valid session. However, if user tries to access the bookmarked page after logout or browser restarts, authenticate
service would return a status code other than 200 (since cookie is not valid), we'll redirect the user to a public zone login page with a message "Your session is invalid, please login first."
The above authenticate check solves the problem when user tries to access a book marked protected page URL, it makes sure access denied and ask user to log in based on security check result.
Now let's talk about the second use case, when different user logs in with a new browser tab, session cookies are replaced with new ones, when switch back to tab one, previously authenticated user security context is not valid any longer. SOHA detects this security context change by a technique of sohaSecureToken
, all the source code are listed above.
More Secure Navigation with SOHA Secure Token
SOHA Secure Token is additional authentication information beyond session key in cookie, it's required to achieve more secured page to page navigation in SOHA. The idea of SOHA Secure Token originates from the use case 2 (discussed in the last section), it has key authentication information based on the principle that a secure service session cannot be reliably identified by cookie, we need other information that is beyond cookie to secure a service session.
SOHA Secure Token raised architecture security bar by introducing the idea that a session cookie must match a value of secure token to confirm a request is secure, the secure token value is generated by a hash algorithm on the service based on the session key, and services set them together when user logs in. If and only if a session cookie is present and valid, and a SOHA Secure Token is included in the request, the service security filter can decide whether the user is authenticated. When either one of them is not present or valid, it would return not authorized. When both of them are there but do not match each other, it also responses not authorized. Only when the security hash algorithm can match the cookie value with the SOHA Secure Token, it authorizes the access to secure service methods. Here below are some details on how it works:
- In the service database user account table, a "salt" value is stored for each user when account is created initially;
- When user successfully logs in, service establishes a session, sets the session key as hashed user id, and also sets it in a session cookie;
- Before the login method returns back to client, it takes the value of "salt" and the session key, runs through a secure hash algorithm to generate the value for SOHA Secure Token, then sets the token value in the response body as
<span style="FONT-SIZE: 10pt">sohaSecureToken</span>
field, returns back to client with status code 200; - When the login success response reaches the client, the
<span style="FONT-SIZE: 10pt">onLoginResult</span>
event handler (listed in source code above) will take the <span style="FONT-SIZE: 10pt">sohaSecureToken</span>
value and store in client by calling <span style="FONT-SIZE: 10pt">$.cbexp.setSOHASecureToken</span>
; - When need to navigate to a new URL securely, the client code calls
<span style="FONT-SIZE: 10pt">$.cbexp.navigateWithToken(newPageBaseURL), </span>
this method will take the baseURL
then append the sohaSecureToken
in the query string
. For example, the result of <span style="FONT-SIZE: 10pt">$.cbexp.navigateWithToken("home.html")</span>
is <span style="FONT-SIZE: 10pt">windows.location.href = "home.html?sohaSecureToken=1m0z3n4g6d7hji90";</span>
Up to now, the login operation completed at service and client, browser sends out the request for home.html, the web server serves it down without hesitate. When browser receives the XHTML template in home.html, our SOHA no-cache mechanism will trigger the execution of security check script (listed above) when page loads, the scripts will grab the sohaSecureToken
from the query string
. If there is nothing found from query string
, it would redirect user and ask for login, otherwise it invokes service method authenticate. (See above source code for more details).
When browser sends out the authenticate
service call, it attaches the session cookie (set by the login
response), and also the sohaSecureToken
is in the request body. When the authenticate
service call reaches the service security filter at app server, here below will happen:
- If session cookie is not present, returns Not Authorized (book marked protected page use case)
- If
sohaSecureToken
in not present, returns Not Authorized (directly request protected resource without a token case) - If session cookie is not valid (sniffed or altered cookie value case) or not associated with any session (session already timed out case), returns Not Authorized
- When session cookie is valid, retrieve user's salt value from associated session state
- Take the session cookie value and user salt, run secure hash algorithm. If the result does not equal to the
sohaSecureToken
value in the request body, returns Not Authorized (browser second tab log in different user case, essentially either cookie value or sohaSecurToken
value get altered case) - Otherwise, both session cookie and
sohaSecureToken
are valid and matched, returns status code 200 to tell the client user already logged in
Now the authenticate
service call returns back to client, it's safe to load header and footer plus other protected page's specific process, user data can be retrieved securely to construct the DOM.
Secure Hyperlinks For Navigation
Up to now, we gain a greater sense of security for navigate programmatically, also reliably handled page refresh, book marked page and altered cookie or URL cases. But SOHA security considerations do not stop here, since user can navigate by clicking the links in the XHTML template. If the links are part of user specific content, it would be handled by the page construction logic by adding sohaSecureToken=[user's soha secure token value]
to its query string, but how about the links in the shared HTML parts? Especially the navigation links in the shared page header? Those a tag's href only points to the base URL, like userPageOne.html, since it's a static
shared HTML file, it doesn't even know about the SOHA Secure Token value at all.
There are two ways to make sure the page links can navigate securely. The first one is to remove all href
attribute altogether from all a
tags, then bind a click
event handler instead. In the click
event handler, it can call $.cbexp.navigateWithToken(newPageBaseURL)
to make sure the token is in the query string
. However, two drawbacks for this approach prompted me to find a better solution.
The first drawback is that the click
event handler is not generic, lots of scripts to write to wire up hyperlinks, prone to errors. Even though event handler can be managed in a generic way, you still need to find out the targeted URL from the a
tag that raises the event; Second shortcoming is that user would lose some default navigation features from browser, like hover a hyperlink to see the targeted URL, Ctrl+Click to open in a new tab rather than replacing the current browser window content, etc.
A better way to secure page hyperlinks for navigation is to patch the href
value in each secure a
tag, it can be easily implemented by HTML and JavaScript, and also preserve the default browser navigation behavior. The only trick is about how to identify a link that points to a protected page, since we do not want to patch any href
that links to public zone page. We solve this issue by simply add a class (named "sohaSecureLink
") to each a
tag in the shared HTML content, then using jQuery
class selector to easily patch them all. For example, the header navigation links HTML may look like this:
<div id="headerNavigation">
<ul class="topNavWrapper">
<li><a href="home.html" class="sohaSecureLink">Home</a></li>
<li><a href="useraccounts.html" class="sohaSecureLink">Accounts</a></li>
<li><a href="useractions.html" class="sohaSecureLink">Actions</a></li>
<li><a href="userassets.html" class="sohaSecureLink">Assets</a></li>
<li><a href="useralerts.html" class="sohaSecureLink">Alerts</a></li>
<li><a href="offerts.html">Offers</a></li>
<li><a href="contactus.html">Contacs</a></li>
</ul>
</div>
And here below is the script to patch them:
patchSecureHrefs: function ()
{
$("a.sohaSecureLink").each(function ()
{
var href = $(this).attr("href");
if (href.indexOf($.cbexp.getSOHASecureTokenQueryString()) < 0)
$(this).attr("href", href + $.cbexp.getSOHASecureTokenQueryString());
});
}
Notice the last two items in the navigation list item do not have the class="sohaSecureLink"
attribute, they are public zone pages, its href
would not be altered by the above script. The end result of patched headerNavigation
HTML in the DOM would be something like this:
<div id="headerNavigation">
<ul class="topNavWrapper">
<li><a href="home.html?sohaSecureToken=1m0z3n4g6d7hji90"
class="sohaSecureLink">Home</a></li>
<li><a href="useraccounts.html?sohaSecureToken=1m0z3n4g6d7hji90"
class="sohaSecureLink">Accounts</a></li>
<li><a href="useractions.html?sohaSecureToken=1m0z3n4g6d7hji90"
class="sohaSecureLink">Actions</a></li>
<li><a href="userassets.html?sohaSecureToken=1m0z3n4g6d7hji90"
class="sohaSecureLink">Assets</a></li>
<li><a href="useralerts.html?sohaSecureToken=1m0z3n4g6d7hji90"
class="sohaSecureLink">Alerts</a></li>
<li><a href="offerts.html">Offers</a></li>
<li><a href="contactus.html">Contacs</a></li>
</ul>
</div>
Since the value of sohaSecureToken
is set by the login
service response, each logged in user would share the same structure of header HTML, but different query string in each protected hyperlink’s href
. Without much HTML and script changes, user can navigate securely to either public or protected zone pages.
Besides session timer coordination, we also solved SOHA's unique navigation challenges in terms of security, the above techniques will keep the authenticated user within their associated session, they can navigate among protected pages with higher level of security (compared to the modal that session only identified by cookie).
When user navigates through each static HTML file (XHTML Template), DOM would get rebuilt when new page loads; previously saved user data (variables in scripts) will be lost. In the case that user input some data in one page, and the data needs to be further processed or displayed in a new page before sending anything back to service for persistence, how do we transfer data from page to page? This is the client data caching aspect of SOHA session management; next section discusses it with details.
SOHA Data Sharing
In addition to navigation security, sharing data among different pages is another unique challenge in SOHA. Regular RIA by Flex/Silverlight/JavaFX doesn't have this concern at all, since most of the cases the entire application is one SWF or XAP files downloaded to the client before it runs, client session data is stored in application data model, always available to each different application views. Even in the modular application scenario, module downloaded at runtime on demand, it still can access the data model stored in shell application. With SOHA, page to page navigation results in DOM rebuilt, there is no application data model that can be accessed by new page.
The first solution that comes to mind might be to ask the service to retrieve the data for the newly loaded page, since it has the session cookie and SOHA secure token, it can retrieve any data associated with the user. But this way will make service not presentation agnostic, because the "page" concept only belongs to presentation; Just bring down the data based on which page loaded will tie the service API with navigation flow. This violates the principle of SOHA that the service should be presentation agnostic and can serve different client applications at same time without knowing anything about navigation flow design. Additionally, for some common data, like user name, phone number that needed by multiple pages, this "asking service to retrieve page data while navigate" method will waste bandwidth, since all those shared data stay the same for all pages.
So the solution needs to be at the client side. For small piece of data, it can be shared via query string, but it's public and everybody can see it. In general, we don't want to put any user specific data in plaint text in query string.
Another approach is to use script read/write cookie at client for shared data. However, cookie usually has 4k size limit, is not reliable when shared data grows. More importantly, this also violates another SOHA principle that the cookie is only set by service and consumed by service. To avoid client code access and consume cookie will help to eliminate some possible interaction problems between client and server down the road.
The solution is to leverage browser built-in local storage. This way will prevent sending incomplete information back to service, and also save bandwidth by not bothering service for common shared user data. jQuery jStoreage plugin leverages HTML5 local storage and uses userData in Internet Explorer (since IE6) for client side data cache. It works great even with some old browsers (IE6+, Firefox 2+, etc.), iPhone Safari and Google Chrome also works nicely. But it has some caveats too.
First one is that jStoreage plugin doesn't work with Opera 10.1 and Safari 3, it has no support for WebKit SQLite API. Another caveat is that even though the browser has localStorage
support, but user can disable it. Since SOHA relies on local storage for data cache and sharing, we need to detect the storage is enabled when application landing page loads, just like we did to detect JavaScript (since it's our Controller and we're building on top of jQuery) and cookie (since service relies on it for authentication). Here below is some code to detect local storage:
isStorageEnabled: function ()
{
var testKey = "cbexp_storage_detector_key";
var testVal = "cbexp_storage_test";
$.jStorage.set(testKey, testVal);
var retVal = (testVal == $.jStorage.get(testKey));
$.jStorage.deleteKey(testKey);
return retVal;
}
When local storage is enabled, the first loaded protected page can retrieve common shared user data from service, then cache it in local storage. When other protected pages loads, after authentication, it can try to retrieve the data from storage. If data is there and valid, then there is no need to call service again for the same data within the session. Just remember when logout or user shuts down browser, we need to flush cached data.
SOHA Ajax Extensions
Service Oriented HTML Application is essentially an extension to Ajax, it enables building a rich interactive HTML application without server page and requires no browser plugin. It extends Ajax by not only wraps the $.ajax
api by $.cbexp.postJason
(see Web App Common Tasks by jQuery for more details) for interactions with services, but also enables asynchronously loading HTML ($.cbexp.loadDivHTML
), CSS ($.cbexp.loadPageCSS
) and JavaScript file ($.cbexp.loadPageScript
) for managing dynamic user preference and experience all by client side logic. This topic is well covered in the last article.
The security check script discussed above plus the SOHA Secure Token mechanism makes sure all Ajax calls and user navigation through static
XHTML files will be performed in a secure while user friendly manner. In SOHA, Ajax not only operates on page level for page regional data interaction, it also functions to create and manage user experiences and application security in the application level. Most of common tasks, like client side master page, client side user controls, consistent page layout and look and feel, conditional contents, navigation logic and redirect process, all become simple and easy to manage.
Wrap Up
To reiterate: Web standards, services and Ajax are the enablers for SOHA, Service Oriented HTML Application. It leverages HTML as Model template, CSS as View and JavaScript as Controller, and utilizes Ajax to interact with web services. It promotes web standards, achieves high scalability by eliminating classic web server pages (JSP, ASP, PHP, Ruby On Rail, ColdFusion, etc…), and also enables rich inter activities without browser plug-ins (Flash/Flex, Silverlight, JavaFX, etc...).
Key concepts and principles of SOHA are:
- High scalability web tier: No HTML generated from server, web tier just serves static files;
- Maximize application reach-ness: No dependency on browser plug-ins, open to next generation of web standards (HTML5, CSS3, etc.), accessible by all browsers and internet-enabled devices;
- Services logic and data access tier stay presentation agnostic;
- Leverage client side processes: Application views will be generated in the client, no server side application logic needed for presentation tier, rich visuals and inter activities are enabled by web standards
- SOA: All dynamic data are retrieved from data service via Ajax, client/server interaction are performed by Ajax calls rather than post back to page itself;
- Clean separation of concerns: Service logic just focus on business logic rather than rendering needs; Web server just serves static HTML/CSS/JS files; client side controller (JS) handles user inter activities and Ajax calls, also manipulates DOM; all application Views are controlled by CSS and static HTML serves as model template;
- Simplified Application common tasks: Dynamic CSS/scripts/HTML loading, secure static HTML based navigation, static page book marking and redirecting, page to page data transferring, extends Ajax API, etc.
- Practical: This new model and architecture has been applied to real world web application project.