Google and Yahoo have JavaScript APIs that let you perform searches. Wikipedia has a JavaScript API that lets you grab data from its pages. These APIs can be accessed cross-domain with a transport method known as JSONP. JSONP works by allowing you add a script tag to your page which points to a URL on their server. The server outputs JavaScript that will call a method (defined as part of the query string in the URL), passing it JSON-formatted data.

You’ll notice that these services are read-only. I don’t currently know of any cross-domain JavaScript APIs that allow you to write data in any meaningful way. An example of this sort of data would be a way, through JavaScript, to update your status on a social networking web site.

Is this lack of functionality simply because developers haven’t taken the time to implement it? To some extent, yes. Almost all of the sites where this would be useful haven’t implemented a JavaScript API at all. At the same time, this has been a pretty tough feature to implement in a way that both protects the user while still making it a relatively painless process.

Right now, several sites have APIs that work through HTTP using HTTP Basic Authentication (e.g. Twitter). If a site were to try to utilize these HTTP APIs through JavaScript, they would have to use their server as a proxy. And in so doing, you would have to trust the server with your username and password.

Why not just check to see if they’re logged in?

This may be better explained through an example. You work for Gaskets, Inc., and your supplier has just sent you an email announcing their new JavaScript API. They explain that in addition to searching their inventory, the API will allow customers to view their order history. You only need to be logged in to your supplier’s site, and the API will work from any other site.

Gaskets, Inc. decides that they don’t currently have the resources to use this API. But meanwhile, over at Evil Gizmos, Ltd., a clever (evil) engineer has decided that the API could prove very valuable to the company.

You see, every time someone visits the home page of Evil Gizmos, Ltd., some JavaScript on the page calls the getOrderHistory API function on the supplier’s site. During your day working at Gaskets, Inc., you browse over to Evil Gizmos, Ltd. to see what they’re up to. When you visit, their site pulls down the complete order history of Gaskets, Inc. and saves it on their server.

How did this happen? Well, your supplier was so excited about providing this new functionality, that they ignored the warnings from their engineers about security. The evil engineer at Evil Gizmo’s Ltd. noticed that, in the email, the only mention of authentication was that you were logged in to their site.

JSONP works by adding a script tag to a page to load data from an external site. Adding a script tag is simply another method of making an HTTP request. When an HTTP request is made by the browser, no matter what site it originates from, all your cookies related to that domain are sent to that site, as part of the official HTTP specifications. If you were logged in to that site, your cookies are what authenticate you and restore your session. No matter what site adds this script tag, the server will always think that you’re logged in. A malicious callback function can accept this data, and use Ajax to save it back to their site.

Just being logged in isn’t enough

First of all, if all you are verifying when sending sensitive data through JSONP is that a user has logged in, stop right now. Does this mean that you’ve just lost the functionality you were hoping to provide? Not totally, the simplest fix is to add a configuration screen to your site that lets a user specify what sites they trust. Then, when you receive an API request, after verifying that a user is logged in, you can compare the referrer specified in the HTTP request against the user’s list of authorized sites.

While this patches the security problem, it requires extra work on the user’s part. Now, in order for someone to use this API, a user must be logged in to the API site first, as well as granting the third-party site API permission.

What should it look like?

Everything should originate from the site you visit, and you shouldn’t be required to leave the page in order to perform your authentication or authorization.

Nowhere should the site you’re visiting be able to access your username and password. And if you arrived at the site already authenticated, you should explicitly authorize the site to have access to that site’s API.

Is this possible?

Really only one possibility exists that fits our requirements. The iframe tag will allow us to nest a page from any URL in a way that obeys same origin policies.

An iframe presents several important properties: It allows you to nest a third-party site in the page, it allows multi-page interaction with the third-party site, and it has security restrictions that prevent the parent and child frames from accessing any data in the other. But most importantly, a single property exists that can transport data between documents: window.name.

Let’s quickly go over the mechanisms that make the exchange of data possible. First, in order for the parent frame to read the window.name property of the child frame, it needs to contain a document in the same domain. During authentication, while the iframe is pointed at a different domain, we have no way of reading the window.name property. Nor can we determine when a user is done authenticating. It’s up to the authenticating server to decide how many steps the user takes in order to log in. For example, if the authenticating server allows a username and password to be entered in the iframe, the user might mistype their password before getting it correct. So we’ll need to make sure that the server has a way to return the iframe to a state in which window.name can be read when the authentication and authorization phase is complete.

We start out by creating an iframe containing an HTML file in the same domain. This file sets window.name to a location that the authenticating server can use to redirect to after authentication and authorization. Once this is done, the iframe gets redirected to the third-party authentication page. On this page, the server can do anything it wants to confirm authorization, but it will ultimately output a script that grabs the redirect location from window.name, rewrites window.name with some information we’ll get to in a second, and then, using window.location, redirects back to the original site. After redirection, using this new value in window.name, authorization can be confirmed, and using the JSONP API in an authenticated way is now possible.

Throughout the whole process, everything you did inside of the iframe was invisible to the originating site.

The library: xauth

In addition to support within the forthcoming Dojo 1.2 release, I’ve written a tiny library that provides a lightweight API and the local file that should be in the iframe when the page loads.

<div id="wrapper">
	<iframe src="js/xauth/blank.html"></iframe>
</div>
<script src="js/xauth/xauth.js"></script>
<script>
	window.onload = function(){
		xauth.init({
			node: document.getElementById("wrapper"),
			url: "http://gaskets.inc/api/approval.php"
		}).addCallback(function(status, token, node){
			alert([status, token, node]);
			node.style.display = "none";
		});
	};
</script>

Breaking it down

We have a wrapper node, which would typically contain an item on the sidebar of your page with a header that makes it clear what the iframe contains (eg. “Authorize Gasket Supplier”). Because we probably want to destroy this node once the user is authorized, it makes sense that we would pass this “wrapper” node to the init function. The function is smart enough to look for the first iframe in the DOM beneath this passed node, and work with that.

Along with that node, we need to specify where to send the user to for authorization.

We’re only adding a single callback here via addCallback, though many can be added. An integer indicating status, a token, and the wrapper node are passed to the function. The status and token will make sense once we cover the server side of things.

The server side of things

I won’t tell you how to write your server-side code (though the Google Code project has some examples. But the important stuff looks like this:

<?php if($authorized): ?>
<html>
<script>
var redirect = window.name;
window.name = "<?php print $token; ?>";
window.location = redirect + "#xauth=1";
</script>
</html>
<?php endif; ?>

Adding the hash xauth=1 is a way to pass status to the callbacks. I like to use 1 to indicate success and 0 to indicate failure, but the codes are up to you and you’re free to give them any meaning that you want.

You should always change the value of window.name, and whatever value you set it to will be passed as the second parameter to the callback. This is useful for providing a token to the user. The idea is that, although you can save a list of trusted sites for a given user, if you would prefer the authorization to take place for every page rather than every site, you can simply create a unique token (bound to the user’s session) that you expect to be passed in any future API calls. But this is completely optional.

Displaying the login form

Though you are free to place a login form inside of the iframe, this might freak some users out. Users that are particularly security conscious, not being able to easily see the URL of the iframe, will likely give up, leave, and not use your service. An easy solution to this is to present a link that will pop up a small login form on your site that automatically closes on successful login, and a “Continue” button that will finish the process up in the iframe.

It’s tempting to worry that using a login form inside of an iframe makes your users more susceptible to phishing attacks. Ultimately, any site could put any form inside of an iframe claiming it’s your login form, with or without your approval. You are slightly better off by not having your login form inside of an iframe because your users will become accustomed to you never having a login form embedded in another page, but there’s really no way to “fix” another site claiming that a user can log in through a form on their page.

“I don’t think I can trust this newfangled technology”

Well, old-timer, none of this is new. Although it might be better said that all the security policies that browser enforces remain in effect using this technique. In fact, the only data that gets exchanged between the two sites involved in the transaction is the data that each site explicitly assigns to what is technically nothing more than a shared variable.

  • Your cookies cannot be read or modified by the third-party site
  • Your HTML is only available to your pages
  • You will always get a reliable HTTP referrer value
  • Your server handles all authentication, period. You can theoretically tell the third-party site that the user is authenticated even if they aren’t. Simply telling them they have authentication doesn’t mean it’s true

The “trick” to this technique is simply utilizing window.name to tell the server where the iframe should be returned to once the authorization is complete, and to use it again to tell the client whether the user approved or denied the authorization request. Everything that matters as far as security goes is up to your server, and there are plenty of options to choose from.

Summarizing server options

  • User does not have a session (is not logged in) with the API site:
    • A form is presented for the user to enter their username and password:
      • On successful login, window.name gets set, iframe is returned to originating site.
      • Optionally, the HTTP referrer might be saved for later automatic authorization.
    • A link is presented along with a “Continue” button:
      • When the user clicks the link, a new window pops up with a login screen.
      • On successful login, window.close(); returns the user to the previous window.
      • Pressing the continue button confirms a valid session window.name gets set, iframe is returned to originating site.
      • Optionally, the HTTP referrer might be saved for later automatic authorization.
  • User has a session (is logged in) with the API site:
    • User has a list of authenticated sites:
      • If the HTTP referrer is in the list, window.name gets set, iframe is returned to originating site.
      • If the originating referrer is not in the list, authorization is requested. Depending on whether “Yes” or “No” is chosen, window.name will likely start with a different status code. In either case, iframe is returned to originating site.
    • If the user does not have a list of authenticated sites, a random token is generated, associated with the user’s session, and placed into window.name after the status code. The iframe is returned to the originating site and future API calls expect to see this token present.

In action

Check out our fictional third-party site that uses Zone Products’s API, and follow the login instructions.

After you’re logged in, visit Sketchy Site, Inc.to see that just because you’ve logged in, a site still needs your explicit approval for you to continue.