Adding 2 Factor Authentication to Your ExpressJS App

Over the last few years barely a week goes by without some mention of a breach at some online organisation and the request to reset passwords does the rounds or we hear about somebody’s account getting compromised.

One method to help reduce the risk of a compromised password is to use something called 2 Factor Authentication. 2FA makes use of the “Something you know, Something you have” approach to authentication. The “Something you know” is your password and the “Something you have” is some form of token. The token is used to generate unique code for each login attempt. There are several different types of token in general use these days.

Hypersecure HyperFIDO token

  • Time based token – These have been around for a while in the form of RSA tokens that companies have been handing out to employees to use with things like VPNs. A more modern variant of this is the Google Authenticator application (Android & iPhone).
  • Smart Card readers – Some banks hand these out to customers, you insert your card into the device, enter your pin number and it generates a one time password.
  • Hardware tokens – These are plugged into your computer and generate a token on demand. An example of these are things like the Yubico neo and the Hypersecur HypeFIDO which implement the Fido U2F standard. At the moment only Chrome supports using these tokens to authenticate with a Website, but there is on going work to add it to Firefox.

In the rest of this post I’m going to talk about adding Google Authenticator and Fido U2F support to a NodeJS/ExpressJS application.

Basic Username/password

Most ExpressJS apps use a plugin middleware called PassortJS to provide authentication. I’ll use this to do the normal username/password stage.

var http = require('http');
var express = require('express');
var passport = require('passport');
var LocalStrategy = require('passport-local').Strategy;
var mongoose = require('mongoose');

var Account = require('./models/account');
var app = express();

var port = (process.env.VCAP_APP_PORT || process.env.PORT ||3000);
var host = (process.env.VCAP_APP_HOST || '0.0.0.0');
var mongo_url = (process.env.MONGO_URL || 'mongodb://localhost/users');

app.set('view engine', 'ejs');
app.use(passport.initialize());
app.use(passport.session());

passport.use(new LocalStrategy(Account.authenticate()));
passport.serializeUser(Account.serializeUser());
passport.deserializeUser(Account.deserializeUser());

mongoose.connect(mongo_url);

app.use('/',express.static('static'));
app.use('/secure' ensureAuthenticated,passport.,express.static('secure'));

function ensureAuthenticated(req,res,next) {
  if (req.isAuthenticated()) {
    return next();
  } else {
    res.redirect('/login');
  }
}

app.get('/login', function(req,res){
  res.render('login',{ message: req.flash('info') });
});

app.post('/login', passport.authenticate('local', { failureRedirect: '/login', successRedirect: '/secure', failureFlash: true }));

app.get('/newUser', function(req,res){
  res.render('register', { message: req.flash('info') });
});

app.post('/newUser', function(req,res){
  Account.register(new Account({ username : req.body.username }), req.body.password, function(err, account) {
    if (err) {
      console.log(err);
      return res.status(400).send(err.message);
    }

    passport.authenticate('local')(req, res, function () {
      console.log("created new user %s", req.body.username);
      res.status(201).send();
    });
  });
});

var server = http.Server(app);
server.listen(port, host, function(){
  console.log('App listening on  %s:%d!', host, port);
});

Here we have a pretty basic Express app that serves public static content from a directory and renders a login page template with ejs that takes a username and password to access a second “secure” directory of static content. It also has a page to register a new user, all the user information is stashed in a MongoDB database using Mongoose.

Google Authenticator TOTP

Next we will add support for the Google Authenticator app. Here there is a PassportJS plugin (passport-totp) that will handle the actual authentication but we need a way to enrol the site into the application. The app has the ability to read configuration data from a QR code which makes setup simple. In the ExpressJS app we need to add the following routes:

var TOTPStrategy = require('passport-totp').Strategy;
var G2FA = require('./models/g2fa');

app.get('/setupG2FA', ensureAuthenticated, function(req,res){
  G2FA.findOne({'username': req.user.username}, function(err,user){
    if (err) {
      res.status(400).send(err);
    } else {
      var secret;
      if (user !== null) {
        secret = user.secret;
      } else {
        //generate random key
        secret = genSecret(10);
        var newToken = new G2FA({username: req.user.username, secret: secret});
        newToken.save(function(err,tok){});
      }
      var encodedKey = base32.encode(secret);
      var otpUrl = 'otpauth://totp/2FADemo:' + req.user.username + '?secret=' + encodedKey + '&period=30&issuer=2FADemo';
      var qrImage = 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=' + encodeURIComponent(otpUrl);
      res.send(qrImage);
    }
  });
});

app.post('/loginG2FA', ensureAuthenticated, passport.authenticate('totp'), function(req, res){
  req.session.secondFactor = 'g2fa';
  res.send();
});

2FA Demo App enrolled in Google Authenticator

The first route checks to see if the user has already set up the Authentication app, if not it generates a new random secret and then uses this to build a URL for Google’s Chart API. This API is used to generate a QR code with the enrolment information. As well as the shared secret, the QR code contains the name of the application and the users name so it can easily be identified within the application. The secret is stashed in the MongoDB database along with the username so we can get it back later.

The second route actually verifies the code provided by the application is correct and adds a flag to the users session to say it passed.

The following code is embedded in the in a page to actually show the QR code then to verify that the app is generating the right values.

googleButton.onclick = function setupGoogle() {
  clearWorkspace();
  xhr.open('GET', '/setupG2FA',true);
  xhr.onreadystatechange = function () {
    if(xhr.readyState == 4 && xhr.status == 200) {
      var message = document.createElement('p');
      message.innerHTML = 'Scan the QR code with the app then enter the code in the box and hit submit';
      document.getElementById('workspace').appendChild(message);
      var qrurl = xhr.responseText;
      var image = document.createElement('img');
      image.setAttribute('src', qrurl);
      document.getElementById('workspace').appendChild(image);
      var code = document.createElement('input');
      code.setAttribute('type', 'number');
      code.setAttribute('id', 'code');
      document.getElementById('workspace').appendChild(code);
      var submitG2FA = document.createElement('button');
      submitG2FA.setAttribute('id', 'submitG2FA');
      submitG2FA.innerHTML = 'Submit';
      document.getElementById('workspace').appendChild(submitG2FA);
      submitG2FA.onclick = function() {
        var pass = document.getElementById('code').value;
        console.log(pass);
        var xhr2 = new XMLHttpRequest();
        xhr2.open('POST', '/loginG2FA', true);
        xhr2.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
        xhr2.onreadystatechange = function() {
          if (xhr2.readyState == 4 && xhr2.status == 200) {
            clearWorkspace();
            document.getElementById('workspace').innerHTML='Google Authenticator all setup';
          } else if (xhr2.readyState == 4 && xhr2.status !== 200) {
            clearWorkspace();
            document.getElementById('workspace').innerHTML='Error setting up Google Authenticator';
          }
        }
        xhr2.send('code='+pass);
      }
    } else if (xhr.readyState == 4 && xhr.status !== 200) {
      document.getElementById('workspace').innerHTML ="error setting up Google 2FA";
    }
  };
  xhr.send();
}

Fido U2F

Now the slightly more complicated bit, setting up the U2F support. This is 2 stage process, first we need to register the token with both the site.

app.get('/registerU2F', ensureAuthenticated, function(req,res){
  try{
    var registerRequest = u2f.request(app_id);
    req.session.registerRequest = registerRequest;
    res.send(registerRequest);
  } catch (err) {
    console.log(err);
    res.status(400).send();
  }
});

app.post('/registerU2F', ensureAuthenticated, function(req,res){
  var registerResponse = req.body;
  var registerRequest = req.session.registerRequest;
  var user = req.user.username;
  try {
    var registration = u2f.checkRegistration(registerRequest,registerResponse);
    var reg = new U2F_Reg({username: user, deviceRegistration: registration });
    reg.save(function(err,r){});
    res.send();
  } catch (err) {
    console.log(err);
    res.status(400).send();
  }
});

The first route generates a registration requestion from the app_id, for a simple site this is just the root URL for the site (it needs to be a HTTPS url). This is sent to the webpage which passes it to the token. The token responds with a signed response that includes a public key and a certificate. These are stored away with the username to be used for authentication later.

app.get('/authenticateU2F', ensureAuthenticated, function(req,res){
  U2F_Reg.findOne({username: req.user.username}, function(err, reg){
    if (err) {
      res.status(400).send(err);
    } else {
      if (reg !== null) {
        var signRequest = u2f.request(app_id, reg.deviceRegistration.keyHandle);
        req.session.signrequest = signRequest;
        req.session.deviceRegistration = reg.deviceRegistration;
        res.send(signRequest);
      }
    }
  });
});

app.post('/authenticateU2F', ensureAuthenticated, function(req,res){
  var signResponse = req.body;
  var signRequest = req.session.signrequest;
  var deviceRegistration = req.session.deviceRegistration;
  try {
    var result = u2f.checkSignature(signRequest, signResponse, deviceRegistration.publicKey);
    if (result.successful) {
      req.session.secondFactor = 'u2f';
      res.send();
    } else {
      res.status(400).send();
    }
  } catch (err) {
    console.log(err);
    res.status(400).send();
  }
});

The authentication is a similar process, the first route generates a random challenge and sends this to the website, which asks the token to sign the challenge. This signed challenge is passed back to the site which checks with the public key/certificate that the correct token signed the challenge.

We should now have a site that allows users to register and then enrol both the Google Authenticator app and a Fido U2F token in order to do 2FA.

All the code is for this demo hosted in Github here and a working example is on AWS here.

Adding Web Bluetooth to the Lightswitch

I’ve just got Web Bluetooth working properly this weekend to finish off my Physical Web lightswitch.

Web Bluetooth is a draft API to allow webpages to interact directly with Bluetooth LE devices. There is support in the latest Chrome builds on Android and can be turned on by a flag: enable-web-bluetooth (Also coming to Linux in version 50.x).

Some folks have already been playing with this stuff and done things like controlling a BB-8 droid from Chrome.

Chrome Physical Web Notification

I started off following the instructions here which got me started. One of the first things I ran into was that in order to use Web Bluetooth the page needs to be loaded from a trusted source, which basically means localhost or a HTTPS enabled site. I’d already run into this with the Physical Web stuff as Chrome won’t show details of a discovered URL unless it points to a HTTPS site and it even barfs on self signed/private CA certificates. I got round this by using a letsencrypt.org (Which reminds me I really need to change my domain registrar so I can get back to setting up DNSSEC).

Now that I was allowed to actually use the API I had a small problem discovering the BLE device. I had initially thought I would be able to filter local devices based on the Primary Services they possessed, something like this:

navigator.bluetooth.requestDevice({
    filters: [{
        services: ['ba42561b-b1d2-440a-8d04-0cefb43faece']
    }]
})

Web Bluetooth Device Discovery

But after not getting any devices returned I had to reach out on Stackoverflow with the this question. This turned out to be because the beacon was only advertising 1 of it’s 3 Primary Services along with the URL. The answer to my question posted by Jeffrey Yasskin pointed me at using a device name prefix and listing the alternative services the device should provide. I’m going to have a look at the code for the eddystone-beacon node to see if it can be altered to advertise more of the services as well as a URL.

navigator.bluetooth.requestDevice({
    filters: [{
        namePrefix: 'Light'
    }],
    optionalServices: ['ba42561b-b1d2-440a-8d04-0cefb43faece']
})

This now allows the user to select the correct device if there are more than one within range. Once selected the web app switches over from making posts to the REST control endpoints to talking directly to the device via BLE. The device is surfacing 2 characteristics at the moment, one for the toggling on and off and one to set the brightness levels.

All the code is up and Github here.

Next I need to see if there is a way to skip the device selection phase if the user has already paired the page and the device to save on the number of steps required before you can switch the lights on/off. I expect this may not be possible for privacy/security reasons at the moment. Even with the extra step it’s still quicker than waiting for the Offical Belkin WeMo app to load.