This week, I added BrowserID login support to my personal, Clojure-based family web hub (code is available as a Git repository at https://matthias.benkard.de/code/benki.git). I was quite astonished to see how easy deploying BrowserID is, especially as compared to OpenID or OAuth. In particular, the deployment does not depend on any BrowserID-specific libraries; any sufficiently featureful HTTP client library will do.

This article describes a moderately naïve implementation of BrowserID on top of an existing user session infrastructure. In particular, it does not deal with live-updating pages after login via JavaScript; instead, it simply reloads the current page when the user has completed the login procedure. (Adding live login capability should be easy enough by simply editing the client-side JavaScript login and logout handlers not to do a full page refresh.)

The client-side JavaScript code can be based on the official tutorial, like the following:


// -*- js-indent-level: 2 -*-

jQuery(function($) {  
  var loggedIn = function(res) {
    console.log(res);
    if (res.returnURI) {
      window.location.assign(res.returnURI);
    } else {
      window.location.reload(true);
    }
  };
  var loggedOut = function(res) {
  };

  var gotAssertion = function(assertion) {
    // got an assertion, now send it up to the server for verification
    if (assertion) {
      $.ajax({
        type: 'POST',
        url: '/login/browserid/verify',
        data: { assertion: assertion },
        success: function(res, status, xhr) {
          if (res === null) {
            loggedOut();
          }
          else {
            loggedIn(res);
          }
        },
        error: function(res, status, xhr) {
          alert("Whoops, I failed to authenticate you! " + res.responseText);
        }
      });
    } else {
      loggedOut();
    }
  }

  $('#browserid').click(function() {
    navigator.id.get(gotAssertion, {allowPersistent: true});
    return false;
  });

  // Query persistent login.
  var login = $('head').attr('data-logged-in');
  if (login === "false") {
    navigator.id.get(gotAssertion, {silent: true});
  }
});

Put that code into a file somewhere and reference it from a script tag after also loading https://browserid.org/include.js and the jQuery library.

Now you need to program your server to reply to AJAX requests to /login/browserid/verify. Let's say you're using Noir and storing users' email addresses in a database table. In addition, I am assuming that your login session management works by putting a :user key into the session map. In this case, your server-side code might look like this (with https://example.com replaced by your site's URI):

(ns eu.mulk.benki.auth
  (:use [hiccup core page-helpers]
        [noir   core])
  (:require [clojure.java.jdbc       :as sql]
            [com.twinql.clojure.http :as http]
            [noir.response           :as response]
            [noir.session            :as session]))

(defpage [:post "/login/browserid/verify"] {assertion :assertion}
  ;; NB.  We delegate verification to browserid.org.
  ;; Can implement this ourselves sometime if we want.
  (let [reply  (http/post "https://browserid.org/verify"
                          :query {:assertion assertion
                                  :audience "https://example.com"}
                :as :json)
        result (:content reply)
        status (:status result)
        email  (:email  result)]
    (if (= (:status result) "okay")
      (sql/with-connection
        (sql/transaction
          (let [record  (first (query "SELECT * FROM user_email_addresses WHERE email = ?" email))
                user-id (and record (:user record))]
            (if user-id
              ;; I'm assuming that your login page stores the desired return
              ;; URI (i.e., the login page's referrer) using flash-put!.
              ;; If it doesn't, you might want to do something slightly different
              ;; here.
              (let [return-uri (session/flash-get)]
                (session/put! :user user-id)
                (response/json {:email email, :returnURI return-uri}))
              ;; If this is a public site, you might want to create a database
              ;; record for the new user here.  We'll be content in denying
              ;; authorization instead.
              {:status 418,
               :headers {"Content-Type" "text/plain"},
               :body "I couldn't find you in the database."}))))
      {:status 418,
       :headers {"Content-Type" "text/plain"},
       :body "Your BrowserID request could not be validated."})))

And that's basically it. The only thing left is to put a sign-in button (<a href="#" id="browserid">Sign in</a>) somewhere on all the pages that need sign-in capability.