The Trials of Browsers
Posted: Wed, 18 July 2007 | permalink | No comments
Since I'm a member of the set of people "somebody better suited to explaining it" mentioned in Silvia Pfeiffer's "A Long Story of Logins", I suppose it falls to me to explain what the hell we actually did to make the thing work.
A quick recap, for anyone who hasn't read Silvia's article: the Vquence website is a very AJAXy, Web 2.0 style video socialisation site. Since we want the site to be available to all as much as possible, we don't lock any part of the site away behind a "login required" page unless we absolutely have to. An unpleasant side effect of this is that, when it does does come time for you to login, pushing you off to a separate "please login" page is going to be, well, a pest. There's all sorts of state about what the user is up to (it's a web-based video mashup creator, after all) that we need to submit -- but only after the user has logged in.
Enter the realm of the Web 2.0 Login. Instead of sending the user to a login page, getting them to login, and then redirecting them back to where they want to go, you just have some sort of popup form that, when submitted to, does a background request and logs the user in behind the scenes.
This isn't a new concept by any means -- for example, try voting up or down a link on reddit when you're not logged in. However, most sites that implement this sort of thing (based on our brief informal survey around the office) don't protect the password information in any way -- it goes over the wire as a plaintext HTTP request. We don't like this, so we wanted to send the login form over HTTPS. And there began our problems.
The trivial implementation of a Web 2.0 Login page is as you'd expect -- the AJAX stalwart, XmlHttpRequest, POSTing to a handler somewhere that validates the login, sets a session, and sends back some javascript to change the page layout a bit to show you're logged in. We had this working nicely, until we started testing on our staging environment, where HTTPS comes into play...
You see, XmlHttpRequest doesn't like making a request to a domain other than the one that the main page comes from -- for security. It also thinks that the same hostname via a different protocol counts as a different domain, for the purposes of this security check. (I can see the theoretical reasoning for it, but it is a bit weird for HTTP to HTTPS transitions) So if you want to POST to https://www.vquence.com/something using XmlHttpRequest, you need to be doing it from a page which is retrieved from somewhere in https://www.vquence.com/ as well. We considered just turning on HTTPS across the board, but that would have all sorts of unpleasant ramifications, so we decided against that. But the problem remained, how do we get the user's credentials to our server in a secure manner with a minimum of unpleasantness?
We came up with all sorts of options. This is where the infamy of "bug #93" started -- it's a monster of a bug log. Lots of it is me going "we could do this, but it'd suck, or we could do this, but it would suck". Finally, Silvia (I think) suggested we look at using an iframe. "Eeew" was the general response, but it might at least give us a way to set a part of the page to HTTPS, which we could use as a launching point for our XmlHttpRequest call.
While researching how to make this all work (I had, to my great pride, never inflicted an iframe on the world before), I managed to come across a few articles on how you can use a hidden iframe to handle requests instead of using XmlHttpRequest. Basically, the trick is that you do a regular POST on your main page, but set the target of the form to a hidden iframe in your page. A general discussion of the principle is available At the Apple Developer site, and there's also a Rails plugin that implements the concept rather nicely. When I found these, I briefly danced for joy. My quest was over. POST to the iframe, the iframe get some JS back from the server, JS mangles the main page's layout to show the user is now logged in... aaaand profit.
Like fun it was. My implementation was short and sweet -- a true work of genius (if I do say so myself). Everything worked like a charm. Until we got it back into staging. Then cross-domain security bit me again. Naturally, if you don't want XmlHttpRequest calls to perform cross-domain, you probably don't want iframes for different sites to be manipulating each other's content either. That's exactly what happened -- the iframe can't manipulate the main page's layout as we need it to. Why I didn't think of this before I charged mightily ahead I don't know. My only defence is that I've never claimed to be a web development guru.
Sneaky tricks will defeat any security measure, though. For instance, we control both sites (http and https), so we can send the user back to the unsecured site after login and then that location (still in the hidden iframe) can manipulate the main page layout. Redirect to the rescue! That worked really well in Firefox, even with HTTPS in staging. We were saved. At least until IE came along. Bloody Microsoft, ruining all my cunning plans.
IE complains when a HTTPS site redirects (using the Location HTTP header) to a HTTP site. You apparently can't turn this off. I was shocked that Microsoft cared so much about their user's data security -- it's certainly not the first thing I think of when I think "Microsoft". My faith in the world was restored when Silvia pointed me to a workaround for the problem -- if you send the redirect in a <meta http-equiv="refresh"> tag instead of a Location header, IE is more than happy to let it pass. Because obvously a chunk of HTML that says "HTTP equivalence" is completely different to a HTTP header. Sheesh.
To snatch defeat from the jaws of victory, after all this brilliant hacking, I managed to make a newbie error with the contents of the http-equiv by setting the content to "0;http://www.vquence.com/blah" instead of "0;url=http://www.vquence.com/blah" (note the url=, because I sure didn't). IE complained by looping infinitely on the source URL, while Firefox just made sense of it and did the right thing. I managed to waste most of an hour of three of us over that little stuff-up. Go me! (Sorry about that guys, by the way)
Feeling very chuffed, I went home for the evening. The next morning saw a browser compatibility defect in my queue -- Safari didn't like the new setup. Instead of doing the right thing, clicking on the login button would create a new tab instead of logging in. The fix? Changing the style of the hidden iframe from "display: none" to "width: 1px; height: 1px; border: 0" did the trick. Fark. So much for "hidden" iframes. What's particularly funny is that Apple's own tutorial on hidden iframes says to avoid "display: none" because NS6 doesn't like it, but doesn't mention it's own glaring deficiency in this area.
So, what are the lessons learned?
- Firstly, IE is different. I knew this already, but living in an all-Linux, not-normally-a-web-frontend-developer world, I need to be reminded every now and then.
- Safari is differently different. Being a former Konqueror user, and wrangling with that browser's retardations in an earlier existence, I should have been ready for this one.
- "Code to the standards" is a crock of shit. That's not to say that we shouldn't encourage it, but I'm fairly sure that the HTML 4.01 standard is silent on whether or not an iframe with it's display property set to none is a valid target for a form submission (I had a quick look, and nothing jumped out at me). Anyone who dismisses the difficulties of web development with a hand wave and a casual "just code to the standards" will get a double-spaced, 24pt printed and bound copy of all of the W3C standards dropped on their head from a tall building.
- No matter what security measure you think has stopped you, there's always a workaround. Can't do a XmlHttpRequest to a different domain? POST to a hidden iframe (you can post to any domain you want, too -- so much for cookie security). Want to run JS you got back from POSTing to a different domain? Just redirect back to the first domain. Can't redirect from a HTTPS to a HTTP page? Don't use a HTTP redirect, use a meta refresh. Sigh.
- Hidden iframes are cool. While I'm firmly in the "frames are evil" camp, they're a nice freaky behind-the-scenes tool if you want to do something more than what a simple XmlHttpRequest gives you. It's like having a whole separate scratch-pad of a browser you can play with, and the user never has to see it.
- Web development is frustrating. I don't do a whole heap of it (I'm more a backend code / infrastructure guy), so when I do some web UI work, I have to re-learn exactly how much of a pain it is to have to cater to the needs of a bunch of different browsers, each with their own quirks and irritations. Suddenly supporting a bunch of different Linux distros seems so much less frustrating.
Post a comment
All comments are held for moderation; markdown formatting accepted.