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.