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="" 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 for an email address:
import urllib import tornado.web import tornado.escape import tornado.httpclient ... @route('/auth/login/browserid/') # ...or whatever you use class BrowserIDAuthLoginHandler(tornado.web.RequestHandler): def check_xsrf_cookie(self): # more about this later pass @tornado.web.asynchronous def post(self): assertion = self.get_argument('assertion') http_client = tornado.httpclient.AsyncHTTPClient() domain = 'my.domain.com' # MAKE SURE YOU CHANGE THIS url = '' 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.
Follow @peterbe on Twitter
Hey, this is great stuff! Thanks for being an early supporter. We have since changed a couple of domains, so: => =>
Also, getVerifiedEmail() is deprecated. Finally, I am no Tornado hacker, but would the "validate_cert" kwarg be available in BrowserIDAuthLoginHandler.post when you call http_client.fetch()? If so, using it would be a Good Thing.
Thanks again for a great blog post!
-- Sam
Thank you so much!
The site that I implemented it on I no longer support but your comment will hopefully help in guiding poor people who end up here.