Protected Cross-Domain Authentication with JavaScript

By on July 30, 2008 12:01 am

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 suppliers 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.

Comments

  • Dion,
    Neil has described a full reusable architecture for accessing protected resources between mutually suspicious parties, this is far beyond GData. GData is a library that Google provides that utilizes some (not all) of these concepts, but largely black boxes the communication (GData has a published protocol for server communication, but not from the browser). This means a user must fully trust the library provided by Google, there is no safety measure against the service provider. This is intended to be used with any service provider (not just Google), untrusted or not, and so it is critical to have the protection that the window.name protocol provides (and GData does not), with open protocol definition that anyone can implement. Second, GData relies on full page redirection for authorization. window.name authorization can do immediate in page (in an iframe) authorization. This can provide a much more fluid user experience. Finally, Neil has also detailed how to separate authentication and authorization aspects of providing access to protected resource. This is an extremely critical part of building secure protected cross-domain accessible web services, and is beyond just what the library provides.

  • Kris, I think the bigger point Dion was making relates to when I said “I don’t currently know of any cross-domain JavaScript APIs that allow you to write data in any meaningful way”

    When I wrote this, I fully expected to hear about a few APIs that do allow writable data, and seeing that Google has it to some extent is good to hear.

  • Great post, Neil. So this will be included in Dojo 1.2, is that correct?

    Is this pretty much the same as IBM’s smash work, or are there differences?

  • No, this is toolkit agnostic,

    Dojo 1.2 will have this type of communication built into the window name transport, though.

  • @Adam: IBM’s smash work is built on fragment identifier messaging (FIM) for inter-frame messaging. We have been investigating using a derivative of the window.name technique for inter-frame messaging and this might be used in lieu FIM for hub style cross-domain widget interaction, but this is still in research.

  • Neil: AOL’s OpenAuth (http://dev.aol.com/api/openauth) does something like this to allow other sites to get an authorization token for AOL/AIM/ICQ and certain OpenID accounts.

    However, since the iframe requires a redirect back to your site, instead of using window.name, it just returns the token as an URL parameter to your page, and it can include auth status (failure type).

    The extra URLS params have the added benefit of avoiding cache issues with the iframe document.

    Also, using an iframe to host the authorization page has problems in Safari, at least in Safari 2 — Safari 3 seems to have Accept All as default for cookies now? At least in Safari 2, if your auth site saves your auth state in a cookie: Safari (with the default options set) would not allow the iframe to set the auth cookie for that auth page in the iframe, since it is considered a 3rd party cookie.

    So I would always open the auth page in a new window. Some folks do not like that UI flow, but it allowed Safari 2 to work, works for people who turn off accepting cookies from other sites, and hopefully gets the user trained to look at the URL bar for validating the site (although most users do not look at it).

    Oh, and BTW (you expected this :), the Web AIM API (http://dev.aol.com/aim/web/serverapi_reference) does allow “write” access via a jsonp API — you can do things like set your status and send IMs. That Web AIM API URL also describes how the API interacts with the auth API provided by OpenAuth.

  • Pingback: SitePen Blog » Protected Cross-Domain Access with Dojo’s windowName()

  • Neil

    Hi,

    I’ve always been wary of relying on the referrer header for security purposes because of referrer spoofing. It sounds like you’ve thought this through, though, so I was wondering if you could expand on the statement, “You will always get a reliable HTTP referrer value.” Is there something about the context in which xauth operates that makes referrer spoofing irrelevant?

    Thanks,

    Neil

  • It’s probably better said that if you’re getting valid cookies, it’s safe to assume that the referrer value is coming from a web browser, and thus be reliable. If someone has stolen your authentication cookie, they don’t need to hack your API, they can use the cookie for direct access to your site.

  • @Neil: My explanation was indeed too vague, using the Referer header is actually rather nuanced due to some of the exploits that are available. First, as nroberts pointed out you should only use it conjunction with cookies (or HTTP authentication, as cookies or authentication mean that the browser user agent has actually authenticated the user, otherwise one can easily just craft their own HTTP requests to exploit a site. Next, you should ensure that you do not have a crossdomain.xml file to prevent cross-domain access through Flash. Older versions of Flash have allowed the Referer header to be modified, and if cross-domain access is allowed requests can be made with forged Referer headers and cookies still present. Next, you should be aware that IE allows the Referer header to be forged in XHR for same-origin requests, but this is not really problem for any real use case, because the same-origin has the default authorization and forging the Referer header should only lower the users authorization level. Finally, you should be aware that some users (very small percentage) disable the Referer header in their browser. If no Referer header is included, the server can’t make the proper authorization decisions and should respond with such a message to the user, indicating that they should enable the Referer header if they want authorization.

  • Or you could do it using easyxss, http://code.google.com/p/easyxss/.
    One of the examples is communicating with a popup from a different domain. This could easily be used for authentication aswell.

  • Manish Goregaokar

    Actually, Wikipedia does have a write api. Its used for certain apps to help editors.

  • Chris Terry

    Hi,

    Not sure exactly what to do on the “server side” … could you explain a bit further?