├── .gitignore ├── index.js ├── package.json ├── lib └── express-browserid.js ├── public └── js │ └── browserid-helper.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./lib/express-browserid"); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-browserid" 3 | , "description": "Pluggable BrowserID helpers" 4 | , "version": "0.0.2" 5 | , "author": "Robin Berjon " 6 | , "dependencies": { 7 | } 8 | , "devDependencies": { 9 | } 10 | , "repository": "git://github.com/darobin/express-browserid" 11 | , "main": "index" 12 | } 13 | -------------------------------------------------------------------------------- /lib/express-browserid.js: -------------------------------------------------------------------------------- 1 | 2 | var path = require("path") 3 | , https = require("https") 4 | , url = require("url") 5 | , qs = require("querystring") 6 | ; 7 | 8 | exports.plugAll = function (app, opts) { 9 | this.plugHelperScript(app, opts); 10 | this.plugVerifier(app, opts); 11 | this.plugLogout(app, opts); 12 | }; 13 | 14 | exports.plugHelperScript = function (app, opts) { 15 | opts = opts || {}; 16 | var route = opts.helperScriptPath || makePath("/js/browserid-helper.js", opts) 17 | , filepath = path.join(module.filename, "../../public/js/browserid-helper.js"); 18 | ; 19 | // XXX we should include strong caching headers here, and 304s for IMS 20 | app.get(route, function (req, res, next) { 21 | res.sendfile(filepath); 22 | }); 23 | }; 24 | 25 | exports.plugVerifier = function (app, opts) { 26 | opts = opts || {}; 27 | var route = opts.verifierPath || makePath("/verify", opts); 28 | app.post(route, function (req, res, next) { 29 | // this code "stolen" from the BrowserID project 30 | var reqParam = url.parse(opts.verifier || "https://verifier.login.persona.org/verify"); 31 | reqParam.method = "POST"; 32 | var vreq = https.request(reqParam, function (vres) { 33 | var body = ""; 34 | vres.on('data', function (chunk) { body += chunk; } ) 35 | .on('end', function () { 36 | try { 37 | var verifierResp = JSON.parse(body) 38 | , valid = verifierResp && verifierResp.status === "okay" 39 | , email = valid ? verifierResp.email : null; 40 | if (req.session) req.session[getSessionKey(opts)] = email; 41 | // if (valid) console.log("assertion verified successfully for email:", email); 42 | // else console.log("failed to verify assertion:", verifierResp.reason); 43 | if (valid) res.json({ status: "okay", email: email }); 44 | else res.json({ status: "error", reason: verifierResp.reason }); 45 | } 46 | catch (e) { 47 | res.json({ status: "error", reason: "Server-side exception." }); 48 | } 49 | }); 50 | }); 51 | vreq.setHeader("Content-Type", "application/x-www-form-urlencoded"); 52 | var data = qs.stringify({ 53 | assertion: req.body.assertion 54 | , audience: opts.audience || req.body.audience 55 | }); 56 | vreq.setHeader("Content-Length", data.length); 57 | vreq.write(data); 58 | vreq.end(); 59 | }); 60 | }; 61 | 62 | exports.plugLogout = function (app, opts) { 63 | opts = opts || {}; 64 | var route = opts.logoutPath || makePath("/logout", opts); 65 | app.all(route, function (req, res) { 66 | if (req.session) req.session[getSessionKey(opts)] = null; 67 | res.json(true); 68 | }); 69 | }; 70 | 71 | function makePath (path, opts) { 72 | var base = opts.basePath || "/browserid"; 73 | return base + path; 74 | } 75 | 76 | function getSessionKey(opts) { 77 | return opts.sessionKey || "email"; 78 | } 79 | -------------------------------------------------------------------------------- /public/js/browserid-helper.js: -------------------------------------------------------------------------------- 1 | 2 | jQuery(document).ready(function() { 3 | "use strict"; 4 | 5 | var $ = jQuery; 6 | var options = { 7 | auto: true 8 | , debug: false 9 | , verifier: "/browserid/verify" 10 | , selector: "#browserid-login" 11 | }; 12 | function audience () { 13 | var scheme = location.protocol 14 | , audience = scheme + "//" + location.hostname 15 | ; 16 | if ("http:" === scheme && location.port && "80" != location.port) audience += ":" + location.port; 17 | else if ("https:" === scheme && location.port && "443" != location.port) audience += ":" + location.port; 18 | return audience; 19 | } 20 | $("script").each(function () { 21 | var $scr = $(this); 22 | if (/browserid-helper\.js(?:\?.+)?$/.test($scr.attr("src"))) { 23 | if ("false" === $scr.attr("data-auto")) options.auto = false; 24 | if ("true" === $scr.attr("data-debug")) options.debug = true; 25 | options.verifier = $scr.attr("data-verifier") || options.verifier; 26 | options.selector = $scr.attr("data-selector") || options.selector; 27 | options.csrf = $scr.attr("data-csrf") || ""; 28 | options.audience = $scr.attr("data-audience") || audience(); 29 | return false; 30 | } 31 | }); 32 | if (options.debug) console.log("[BrowserID] Options: ", options); 33 | var $win = $(window); 34 | $(options.selector).click(function () { 35 | $win.trigger("login-attempt"); 36 | navigator.id.get(function (assertion) { 37 | $win.trigger("login-response", assertion); 38 | if (assertion) { 39 | $win.trigger("received-assertion", assertion); 40 | var data = { audience: options.audience, assertion: assertion }; 41 | if (options.csrf) data._csrf = options.csrf; 42 | $.post( 43 | options.verifier 44 | , data 45 | , function (data) { 46 | if (!data) $win.trigger("login-error", "no verify data"); 47 | if ("okay" === data.status) { 48 | $win.trigger("login-success", data); 49 | } 50 | else { 51 | $win.trigger("login-error", ["verify error", data]); 52 | } 53 | } 54 | ), "json"; 55 | } 56 | else { 57 | $win.trigger("login-error", "browserid error"); 58 | } 59 | }); 60 | }); 61 | if (options.debug) { 62 | $win.on("login-attempt", function () { 63 | console.log("[BrowserID] attempting to log in"); 64 | }); 65 | $win.on("login-response", function (ev, ass) { 66 | console.log("[BrowserID] login responded with assertion: " + (ass ? ass : "*none*")); 67 | }); 68 | $win.on("received-assertion", function (ev, ass) { 69 | console.log("[BrowserID] assertion received: " + ass); 70 | }); 71 | $win.on("login-error", function (ev, type, data) { 72 | console.log("[BrowserID] error: " + type, data); 73 | }); 74 | $win.on("login-success", function (ev, data) { 75 | console.log("[BrowserID] success!", data); 76 | }); 77 | } 78 | if (options.auto) { 79 | $win.on("login-success", function (ev, data) { 80 | location.reload(); 81 | }); 82 | } 83 | }); 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## Overview 3 | 4 | This is a set of helper code that intends to make using [BrowserID](https://login.persona.org/) easier 5 | from inside an Express application. I slapped it together because as I started integrating BrowserID 6 | support into one of my project, I realised that while there were great pieces of code as well as 7 | documentation and examples, most of them using NodeJS already, a decent amount of assembly was 8 | required in order to actually use it (either that, or I didn't find the right pieces ;). That's 9 | not surprising: this is very new technology, and you don't expect it to come in a velvet-padded 10 | case. 11 | 12 | `express-browserid` tries to fill that gap. I cannot say that it has seen much production use at this 13 | point since it has essentially been producing by me pasting the code I had used to explore 14 | BrowserID into a module but it ought to be simple to use. 15 | 16 | It works by adding its own set of routes to your Express `app` that can: 17 | 18 | * verify that a browser assertion is valid and set the email in the session; 19 | * logout (i.e. remove the email from the session); 20 | * send a helper client-side script. 21 | 22 | The client-side script knows how to trigger a BrowserID sign-in and contact the verify route 23 | to check that the sign-in is valid. It is easily configurable, and emits events throughout its 24 | lifecycle to enable UI code to react to the sign-in's progress. 25 | 26 | ## Usage (Server) 27 | 28 | Somewhere as part of your routes (i.e. after configuration): 29 | 30 | require("express-browserid").plugAll(app); 31 | 32 | This method supports options, and there are other methods that will plug individual routes on their 33 | own rather than all of them at once. Details below. 34 | 35 | Note that the library uses POST, so Express must be configured prior to initialization with a body 36 | parser: 37 | 38 | app.use(express.bodyParser()); 39 | 40 | ## Usage (Client) 41 | 42 | Include the following script anywhere after having included jQuery and 43 | https://login.persona.org/include.js. 44 | 45 | 46 | 47 | The path to the script can be configured on the server side (or if you prefer, you can load it from your 48 | own public directory — but you don't have to). All the data-* attributes that configure its behaviour are 49 | optional, and there are more than just the ones listed above (details below). 50 | 51 | ## Installation 52 | 53 | $ npm install express-browserid 54 | 55 | ## Interface (Server) 56 | 57 | The NodeJS side of the code supports the following methods: 58 | 59 | * plugAll(app, [opt]). Calls all of the methods below in turn in order to plug all the routes into the app. 60 | It takes an Express `app` object (required) and optionally a dictionary of options which are passed on to 61 | the following methods. 62 | * plugHelperScript(app, [opt]). Creates the GET route for the helper script. That defaults to 63 | `/browserid/js/browserid-helper.js` unless overridden using the options described below. 64 | * plugVerifier(app, [opt]). Creates the POST route for the sign-in verification operation. That defaults to 65 | `/browserid/verify` unless overridden using the options described below. 66 | * plugLogout(app, [opt]). Creates the "all" route (it works for any HTTP method) that logs the user out by 67 | unsetting the `email` key in the session. Defaults to `/browserid/logout` unless overridden using the options 68 | described below. 69 | 70 | The methods above accept the following options. `plugAll` accepts them all and passes them on, the others accept 71 | the ones that make sense to them. 72 | 73 | * basePath. The root path under which the other routes are set up. It defaults to `/browserid` (note the leading 74 | `/`). If you change it to `/foo` then logout becomes `/foo/logout`, the verifier `/foo/verify`, etc. 75 | * helperScriptPath. The route for the client-side helper script. This overrides not just the default value, but 76 | also the value built using `basePath`. That is to say, if you set `basePath` to `/foo` and `helperScriptPath` 77 | to `/bar/browserid-helper.js` then the route will be `/bar/browserid-helper.js` and won't take `/foo` into 78 | account. IMPORTANT NOTE: currently, this path has to end with `browserid-helper.js` otherwise the script won't 79 | be able to accept configuration correctly. This may change. 80 | * verifierPath. The route to the verify operation. The same note applies for overriding as in `helperScriptPath`. 81 | * logoutPath. The route to the logout operation. The same note applies for overriding as in `helperScriptPath`. 82 | * verifier. The URL for the verification service to use, defaults to "https://browserid.org/verify" but you can 83 | use any service that follows the specification. 84 | * audience. The scheme + host + optional port (e.g. `http://berjon.com` or `https://github.com:8001`) for which 85 | this authentication is being made. Only override this if you know what you're doing, the client side is a better 86 | place to configure this setting (if configured there, it will be used here). 87 | * sessionKey. The name of the key used to store the email in the session. Defaults to `email`. 88 | 89 | ## Interface (Client) 90 | 91 | The client-side helper script can work out of the box with no configuration whatsoever, assuming you adhere to some 92 | basic conventions: change none of the server-side routes, and use `#browserid-login` as the ID of the element on 93 | which users click to initiate login. 94 | 95 | If you need specific behaviour, you'll have to using the configuration attributes. These are placed on the script 96 | element itself, and comprise: 97 | 98 | * data-auto. By default, once the login is successful, the page is simply reloaded. The assumption there is that your 99 | server-side code will want to regenerate the page based on the newly established user identity. But that's not always 100 | the case, for instance you may have written a web application in which the client always gets the same code, but needs 101 | to authenticate to access specific APIs. In that case you can disable the reload behaviour by setting `data-auto` 102 | to `false`. 103 | * data-debug. Set this to `true` to get extra information dumped to the `console`. 104 | * data-verifier. The URL of the verifier service you want to use. Note that this is probably on your server unless you 105 | are confident you can work with cross-domain requests. 106 | * data-selector. A jQuery selector that picks the element(s) on which the user can click to activate the BrowserID 107 | sign-in process. Defaults to `#browserid-login`. 108 | * data-csrf. If your server has CSRF protection enabled (which it should) then set your CSRF token here. The simplest 109 | way is to use [the `express-csrf-plug` module](https://github.com/darobin/express-csrf) (shameless plug) for this 110 | as it will both enable CSRF and make a `csrf` variable available in your views which you can assign to this attribute. 111 | * data-audience. The BrowserID audience you wish to authenticate for. This will default to a guess made based on the 112 | current page's location (which I think should generally be correct — but don't just take my word for it, try it!). 113 | 114 | As the script progresses through the various phases of the sign-in, it dispatches events on the `window` object. You can 115 | listen to them using `$(window).on("event-name", function (...) { ... })`. You don't have to listen to any of these, 116 | but it is recommended that you listen for `login-error`. The reason for this is that the default behaviour for errors is 117 | to do nothing, which isn't entirely user-friendly. If you've set `data-auto` to `false`, you probably want to listen for 118 | `login-success` as well since you'll likely want to do something at that moment. The other events are mostly either for 119 | debugging, or for displaying progress to the user if you so desire. 120 | 121 | All events receive an event object as their first parameter, as always with jQuery. When additional parameters 122 | are discussed below, they are passed *after* that event object. 123 | 124 | The following events are dispatched: 125 | 126 | * login-attempt. The user has clicked on the piece of UI that starts the sign-in process. The BrowserID dialog 127 | should be showing up about now. 128 | * login-response. The BrowserID service has replied. This does not indicate that the reply was successful. If 129 | we received an assertion back from the service, it is passed to the handler. 130 | * received-assertion. The `login-response` above was indeed successful and an assertion was received. It is passed 131 | to the handler. 132 | * login-error. There was an error at some stage. The handler gets a string indicating the error type, and a 133 | if applicable additional data that was received and which can be used for debugging purposes. The possible error 134 | types are: `no verify data` indicates that the verification service failed so that the assertion was probably 135 | invalid (or something is seriously amiss); `verify error` indicates that there was a verification error, in 136 | which case the reason is given in the `reason` attribute of the additional data parameter; `browserid error` 137 | means that there was a problem contacting the BrowserID service (e.g. you're offline). 138 | * login-success. The login was successful. The handler receives an additional data object that at least has 139 | an `email` attribute featuring the login email. 140 | 141 | ## License 142 | 143 | (The MIT License) 144 | 145 | Copyright (c) 2012 Robin Berjon <robin@berjon.com> 146 | 147 | Permission is hereby granted, free of charge, to any person obtaining 148 | a copy of this software and associated documentation files (the 149 | 'Software'), to deal in the Software without restriction, including 150 | without limitation the rights to use, copy, modify, merge, publish, 151 | distribute, sublicense, and/or sell copies of the Software, and to 152 | permit persons to whom the Software is furnished to do so, subject to 153 | the following conditions: 154 | 155 | The above copyright notice and this permission notice shall be 156 | included in all copies or substantial portions of the Software. 157 | 158 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 159 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 160 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 161 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 162 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 163 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 164 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 165 | --------------------------------------------------------------------------------