BrowserID is a new single sign-on initiative lead by Mozilla that takes a very refreshing approach to single sign-on. It's basically like OpenID except better and similar to the OAuth solutions from Google, Twitter, Facebook, etc but without being tied to those closed third-parties.
At the moment, BrowserID is ready for production (I have it on Kwissle) but the getting started docs is still something that is under active development (I'm actually contributing to this).
Anyway, I thought I'd share how to integrate it with Tornado
First, you need to do the client-side of things. I use jQuery but that's not a requirement to be able to use BrowserID. Also, there are different "patterns" to do login. Either you have a header that either says "Sign in"/"Hi Your Username". Or you can have a dedicated page (e.g. mysite.com/login/
). Let's, for simplicity sake, pretend we build a dedicated page to log in. First, add the necessary HTML:
<a href="#" id="browserid" title="Sign-in with BrowserID">
<img src="/images/sign_in_blue.png" alt="Sign in">
</a>
<script src="https://browserid.org/include.js" async></script>
Next you need the Javascript in place so that clicking on the link above will open the BrowserID pop-up:
function loggedIn(response) {
location.href = response.next_url;
/* alternatively you could do something like this instead:
$('#header .loggedin').show().text('Hi ' + response.first_name);
...or something like that */
}
function gotVerifiedEmail(assertion) {
// got an assertion, now send it up to the server for verification
if (assertion !== null) {
$.ajax({
type: 'POST',
url: '/auth/login/browserid/',
data: { assertion: assertion },
success: function(res, status, xhr) {
if (res === null) {}//loggedOut();
else loggedIn(res);
},
error: function(res, status, xhr) {
alert("login failure" + res);
}
});
}
else {
//loggedOut();
}
}
$(function() {
$('#browserid').click(function() {
navigator.id.getVerifiedEmail(gotVerifiedEmail);
return false;
});
});
Next up is the server-side part of BrowserID. Your job is to take the assertion that is given to you by the AJAX POST and trade that with https://browserid.org for an email address:
import urllib
import tornado.web
import tornado.escape
import tornado.httpclient
...
@route('/auth/login/browserid/')
class BrowserIDAuthLoginHandler(tornado.web.RequestHandler):
def check_xsrf_cookie(self):
pass
@tornado.web.asynchronous
def post(self):
assertion = self.get_argument('assertion')
http_client = tornado.httpclient.AsyncHTTPClient()
domain = 'my.domain.com'
url = 'https://browserid.org/verify'
data = {
'assertion': assertion,
'audience': domain,
}
response = http_client.fetch(
url,
method='POST',
body=urllib.urlencode(data),
callback=self.async_callback(self._on_response)
)
def _on_response(self, response):
struct = tornado.escape.json_decode(response.body)
if struct['status'] != 'okay':
raise tornado.web.HTTPError(400, "Failed assertion test")
email = struct['email']
self.set_secure_cookie('user', email,
expires_days=1)
self.set_header("Content-Type", "application/json; charset=UTF-8")
response = {'next_url': '/'}
self.write(tornado.escape.json_encode(response))
self.finish()
Now that should get you up and running. There's of couse a tonne of things that can be improved. Number one thing to improve is to use XSRF on the AJAX POST. The simplest way to do that would be to somehow dump the XSRF token generated into your page and include it in the AJAX POST. Perhaps something like this:
<script>
var _xsrf = '{{ xsrf_token }}';
...
function gotVerifiedEmail(assertion) {
// got an assertion, now send it up to the server for verification
if (assertion !== null) {
$.ajax({
type: 'POST',
url: '/auth/login/browserid/',
data: { assertion: assertion, _xsrf: _xsrf },
...
</script>
Another thing that could obviously do with a re-write is the way users are handled server-side. In the example above I just set the asserted user's email address in a secure cookie. More realistically, you'll have a database of users who you match by email address but instead store their database ID in a cookie or something like that.
What's so neat about solutions such as OpenID, BrowserID, etc. is that you can combine two things in one process: Sign-in and Registration. In your app, all you need to do is a simple if statement in the code like this:
user = self.db.User.find_by_email(email)
if not user:
user = self.db.User()
user.email = email
user.save()
self.set_secure_cookie('user', str(user.id))
Hopefully that'll encourage a couple of more Tornadonauts to give BrowserID a try.