2-Factor All The Things! (with node-2fa)

In addition to a strong password, using 2-factor authentication (2FA) should be considered mandatory when doing non-trivial things on the internet.  That means when personal data, privacy, storage, social networks, money, reputation, or home automation products come into play - basically all the time.  

In the uphill battle to keep your online accounts secure from bad actors, there is no other mechanism that is as effective.  Without 2-factor authentication, trust in the security of the internet would buckle beneath a pervasive load of hacking and fraud.

Enter One-Time Passwords

( A 2FA token is a type of One Time Password, a value generated from a secret and only valid for a short period of time )

The technologies that power 2FA are open source, widely backed, supported by large tech companies, and more standardized than I expected. This means that it is easier than ever before for developers to integrate 2FA into software applications of all types.

The majority (citation needed) of 2FA implementations use a TOTP ( Time-Based One-Time Password Algorithm ). There are several popular apps available across mobile device platforms that facilitate scanning QR secrets, storing them, and generating tokens ready to be typed by the user.  I'll be using Google Authenticator, but it doesn't matter.

OTP POC

In this article I'm going to setup the most basic application that will touch all the bases in this workflow.  I'll try to contextualize, but you'll need to use your imagination a little to fit it into your application.

This will be a node script and npm led me to discover node-2fa, quite a cool little library.  Let's get to the code, description later:

const prompt = require('prompt')
const twofactor = require('node-2fa')

prompt.start()

const newSecret = twofactor.generateSecret({ name: "2FA POC", account: "mroboto" });
console.log(newSecret)

async function run () {
    // prompt the user for a name and token
    const {name,token} = await prompt.get(['name', 'token'])
    
    // check validity of the provided token
    const verified = twofactor.verifyToken(newSecret.secret, token, 2); // +/-2 minute window
    
    if (!verified) { // null if not valid
        console.log(`${name} is a bum`)
    } else {
        console.log(`${name} is alright - valid token 👍`, verified)
    }
    console.log('fin')
}
run()
Shoutout to the prompt library for helping with the command-line I/O

Running that, we'll first see the output of the new secret generation:

The first line, secret: '4G6HY2YAQBH5QBRODC74MJETJ6VE34HB', is the random value upon which all the rest of the crypto-magic will be based.

The highlighted URL provides the QR code.  Notice how a google API is used to generate the QR image? Thanks Google.  On a desktop, clicking that link loads in the browser:

Not great UX.

However, it would be much more elegant to use this URL as the src for an <img> tag, allowing it to be embedded into any context

<img src="https://chart.googleapis.com/chart?chs=166x166&amp;chld=L|0&amp;cht=qr&amp;chl=otpauth://totp/2FA%20POC%3Amroboto%3Fsecret=4G6HY2YAQBH5QBRODC74MJETJ6VE34HB%26issuer=2FA%20POC">

Next step is to scan that QR code into Google Authenticator ( or similar ).  For this part I have to use canned screenshots because my phone's security policy won't let me screenshot for ... security reasons.

Add a new secret with this button, get camera ready to scan QR code

Scan the QR square secret using the device camera and a new entry should appear in the app with the name of the app as defined in code "2FA POC".

Back on the command line, type any name and then a valid token generated by Google Authenticator for "2FA POC":

I said this example was the bare minimum

While absolutely useless, this script did get me thinking about ephemeral secrets ( only existing for the lifetime of the application ).

The other output shown above from the secret generation: ( uri: 'otpauth://totp/2FA%20POC%3Amroboto?secret=4G6HY2YAQBH5QBRODC74MJETJ6VE34HB&issuer=2FA%20POC' ) is the metadata encoded along with the secret as some standard formatted with otpauth:// schema.  This is the machine-to-machine format for passing the secret.

In Practice

There are no mistakes in the math of cryptography, but there are countless ways to implement it incorrectly or improperly secure secrets.  

In a real world situation every individual user needs their own secret generated and stored in a database that itself should be encrypted.  A user-specific 'pin' can also be required to be prepended for additional security, making the whole input look like: <PIN><token>

QR codes / secrets should only be made available to individual users at the time of creation. Ideally the user can't recover their secret. Most workflows I've seen just allow establishing a new secret but I'll admit I don't know the security rationale behind that workflow.

A strong first factor (passwords) is still necessary. Even after implementing 2FA , best practices like strong password enforcement and password rotation are still good ideas.  Expiring secrets after a certain period of time or length of inactivity is another idea.

In Conclusion

Use 2FA wherever it is offered online.  If you make software that handles data for users, strongly consider offering it.