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.
Comments
Submit a comment
Note: This website uses a JavaScript-based spam prevention system. Please enable JavaScript in your browser to post comments. Comment format is plain text. Use blank lines to separate paragraphs.