Upgrading and Implementing Webmaker SSO

For the past three weeks, My teammates and I have been working hard on an exciting new enhancement for the Webmaker project. After many hours of researching, whiteboarding, OMGWTF's and many Anakin Skywalkers, we've finally shipped the new system to production.

If you want to skip directly to the implementation guide, click one of the following:

Dependencies

Server Configuration

Front End Configuration (HTML+CSS)

Front End Configuration (JavaScript)

In order to fully understand what we've done, you will need to understand where we came from.

When we set out to build Webmaker in April of 2013, One of the requirements was to have a single sign on mechanism across the Webmaker site and tools. In order to accomplish this in the time we had, we were forced to move very quickly. The result was a log-in system, built with Persona, that accomplished the task. Unfortunately, it was a nightmare to implement and caused us many issues.

This log-in system spread its dependencies across two applications. This meant that if you want to develop popcorn maker, you have to be running webmaker.org and a Login Server to log in. The limitations of this system became apparent almost immediately.

Logging in centred around storing a session cookie on one subdomain, and checking for that cookie on all other subdomains through a complicated iframe post message API. This created an obvious lag between page load and sign-in and proved to be a complete nightmare to implement.

Today, I'm going to go over the new Webmaker authentication API, and guide you through an implementation using real, working code (available on Github!). This guide will teach you how to implement the system in a node server that's running the express web framework, using nunjucks as the template engine as well as how hook up your front end to the system.

Before I begin, we need to get a basic understanding of the system, and all the parts involved. In the log-in process, there are four parties.

  • The browser or "client"
  • The App Server or "App"
  • The "Login Server"
  • The Persona Identity Provider or "Persona"

This scenario will assume the user has already created a Webmaker Account. When the client application initiates the log in process, they will be presented with a typical persona log-in pop-up. After completing the Persona sign in process, the client will get what's known as an assertion (a cryptographically signed piece of text that proves the user is the owner of a specific email address). The Client will send this assertion, along with the email and the App's hostname, to an endpoint on the App, typically '/verify'.

The verify endpoint will forward the assertion, email and hostname to the Login Server. Upon receipt of the request, the Login Server will verify the assertion with Persona, and determine whether or not this is a valid log-in. If it is, the user's account information (username, email, etc) is returned to the App. If the request checks out, the App will create and sign a session cookie and return the cookie to the client. Once the client has the cookie, the user has successfully logged in!

The special part of this whole system is that the cookie which is assigned to the client is what we refer to as a "Super Cookie". The "super" indicates that it can be sent on connections to subdomains of a parent subdomain. For example, a cookie for webmaker.org can be sent on connections to all apps that exist on a subdomain of it - ie. popcorn.webmaker.org and thimble.webmaker.org. This enables apps to detect valid sessions without having to check yuing a complicated iframe post-message API.

Implementation

Lets implement this login strategy!

Dependencies

Lets start with the npm requirements for setting up your express server. At a minimum, you will need:

Optionally, you can use habitat for managing your environment variables and helmet for adding security header middleware (recommended!)

The back and front ends of your app will need the webmaker-auth-client library. If you're using a front-end package manager like bower, you can include it in your bower.json file as a dependency.

A sample implementation lives at https://github.com/mozilla/webmaker-login-example and will serve as the basis for this guide. It is not unlikely that one day this guide will be out of date, so be sure to read the documentation of the Login Server, webmaker-auth-client and webmaker-auth repositories to be sure that nothing has changed significantly.

Server configuration

Here I'll describe the important things to do when setting up your server. You can find the full app.js file here.

Step 1:

Load the Webmaker-auth npm module
var WebmakerAuth = require('webmaker-auth');

View Step 1 on Github

Step 2:

Add the bower_components directory to your nunjucks path.

This will let you include the create-user-form html snippet in your views.

var nunjucks = require('nunjucks');
var nunjucksEnv = new nunjucks.Environment([
  new nunjucks.FileSystemLoader(__dirname + '/views'),
  new nunjucks.FileSystemLoader(__dirname + '/bower_components')
], {
  autoescape: true
});

View Step 2 on Github

Step 3:

Add an instantiate filter to your nunjucks environment

This is required because the create-user-form is localised.

nunjucksEnv.addFilter("instantiate", function(input) {
    var tmpl = new nunjucks.Template(input);
    return tmpl.render(this.getVariables());
});

View Step 3 on Github

Step 4:

Instantiate and add the webmaker-i18n and webmaker-locale-mapping middleware to your express instance
app.use(i18n.middleware({
  supported_languages: env.get("SUPPORTED_LANGS"),
  default_lang: "en-US",
  mappings: require("webmaker-locale-mapping"),
  translation_directory: path.resolve(__dirname, "locale")
}));

View Step 4 on Github

Step 5:

Merge the create user form localisations with the app's localisations using i18n.addLocaleObject().
i18n.addLocaleObject({
  "en-US": require("./bower_components/webmaker-auth-client/locale/en_US/create-user-form.json")
}, function (result) {});

View Step 5 on Github

Step 6:

Setup the webmaker-auth middleware module.
For production/staging environments, you can specify a domain parameter, enabling SSO for apps hosted on the same domain (they must be configured with the same session secret, otherwise they won't be able to decrypt cookies issued by one another.)
var login = new WebmakerLogin({
  loginURL: env.get('LOGIN_URL'),
  secretKey: env.get('SECRET_KEY'),
  domain: env.get('DOMAIN', null),
  forceSSL: env.get('FORCE_SSL', false)
});

View Step 6 on Github

Step 7:

Set up webmaker-auth's cookie parser and cookie session middleware
app.use(login.cookieParser());
app.use(login.cookieSession());

View Step 7 on Github

Step 8:

Set up less-middleware to compile the webmaker-auth-client's CSS
var optimize = env.get('NODE_ENV') !== 'development',
    tmpDir = path.join(require('os' ).tmpDir(), 'mozilla.webmaker-login-example.build');

app.use(lessMiddleware({
  once: optimize,
  debug: !optimize,
  dest: tmpDir,
  // NOTE: we'll use a LESS include in our public/css to include the create user form styles
  src: __dirname + '/public',
  compress: optimize,
  yuicompress: optimize,
  optimization: optimize ? 0 : 2
}));

View Step 8 on Github

Step 9:

Set up routes for logging in, using the webmaker-auth route handler functions.
If you don't need to add middleware to the login routes, you can use the .bind() function, which accepts your express instance as a parameter and automatically sets up routes.
app.post('/verify', login.handlers.verify);
app.post('/authenticate', login.handlers.authenticate);
app.post('/create', login.handlers.create);
app.post('/logout', login.handlers.logout);
app.post('/check-username', login.handlers.exists);

View Step 9 on Github

Step 10:

Statically serve the bower_components folder, so the front end can load webmaker-auth-client.js
app.use('/bower', express.static(__dirname + '/bower_components'));

View Step 10 on Github

Step 11:

Add the app's hostname and port to the Login Server's ALLOWED_DOMAINS variable.
This is an external configuration step that must be done to the login server you wish to connect with.

Front End Configuration (HTML+CSS)

Here we'll configure the view to work with our login strategy. Check out the full file here, and check out the CSS file here

Step 1:

Load in the create user form css
In this example, the create user form CSS is imported into the a separate CSS file using a less import directive - you do not necessarily have to load the new user form css in this way.
<link href="/css/login-example.css" rel="stylesheet">
// Import the create user form less.
// NOTE: the "(less)" import directive forces less to interpret the file as LESS, regardless
// of the ".css" extension on the file. This means you must use LESS > v1.3
@import (less) "../../bower_components/webmaker-auth-client/create-user/create-user-form.css";

View Step 1 (html) on Github

View Step 1 (css) on Github

Step 2:

Add some log-in and log-out buttons
<button class="btn btn-primary login">Login</button>
<button class="btn btn-warning logout">Logout</button>

View Step 2 on Github

Step 3:

Using nunjucks, include the create user form
{% include "/webmaker-auth-client/create-user/create-user-form.html" %}

View Step on Github

Step 4:

Load the Persona include.js script
<script src="https://login.persona.org/include.js"></script>

View Step 4 on Github

Step 5:

If not using the minified version of webmaker-auth-client, you must include EventEmitter.js
<script src="/bower/eventEmitter/EventEmitter.js"></script>

View Step 5 on Github

Step 6:

Load webmaker-auth-client.js
You can also load webmaker-auth-client with require-js
  <script src="/bower/webmaker-auth-client/webmaker-auth-client.js"></script>

View Step 6 on Github

Step 7:

Load in the JS for setting up webmaker-auth actions and event listeners
<script src="/js/login-example.js"></script>

View Step 7 on Github

Front End Configuration (JS)

Now lets write some JavaScript that uses the webmaker-auth-client to log in a user. View the entire file here

Step 1:

Instantiate the WebmakerAuthClient
For a list of all options check out the webmaker-auth-client documentation
var auth = new WebmakerAuthClient({});

View Step 1 on Github

Step 2:

Create login, logout and error event listeners
auth.on('login', function(data, message) {
  usernameEl.innerHTML = data.email;
});

auth.on('logout', function() {
  usernameEl.innerHTML = '';
});

auth.on('error', function(err) {
  console.error(err);
});

View Step 2 on Github

Step 3:

Call the verify function
Verify checks with the server to see if the client currently has a valid session, and will trigger a login event or logout event based on the result.
auth.verify();

View Step 3 on Github

Step 4:

Hook the logout and login functions up to the buttons
loginEl.addEventListener('click', auth.login, false);
logoutEl.addEventListener('click', auth.logout, false);

View Step 4 on Github

All Done!

The sum of all these steps is a simple app that can log in a webmaker user!

Some may say that this is still a lot of work, but to be perfectly honest, the previous SSO solution was so bad that I'm certain that simply writing a guide for it would have been a massive undertaking.

We're always going to be improving, so as we go, hopefully some of the rougher edges here disappear. We're always open to feedback and suggestions, so let us know what you think! And as always, file bugs if there are any problems.