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.