cft

3 Easy Steps to Implement Two-Factor Authentication in Node.JS

2FA isn’t as scary to implement as you think, and I’ll prove it to you in 3 steps.


user

Landy Simpson

3 years ago | 7 min read

Two Factor Authentication (2FA) is a second layer of account protection, which I highly recommend everyone should enable. Unlike the traditional username/password, 2FA requires the user to enter an additional piece of information related to something personal.

For example, the most well-known form of 2FA requires an end-user to answer a security question. If the end-user is the one who set-up the security question, they should have no problem answering the question. The security question could be: What’s your mothers maiden name? What was the name of your first pet? What the name of your hometown?

The most recent forms of 2FA is receiving a token via email or SMS, or getting a token via an Authenticator application, such as Google Authenticator.

The server generates a secret key against a users account. And the secret key helps generate subsequent tokens the end-user uses to verify their identity. If the end-user is the holder of the account, then they should know where to look for the token (SMS, email, or Authenticator app), and the token should be verifiable against the secret.

There are a couple of ways to implement 2FA, but this article will review implementing 2FA using the npm package, speakeasy.

First things first, let’s do a quick review of OTP.

What is OTP (One Time Password)?

One-Time Password is precisely as the name suggests, it’s a password the end-user uses once. I like calling them tokens. Unlike traditional passwords created and known by the end-user, the server or Authenticator app generates an OTP. One Time-Passwords rely on two things: a secret and a moving factor.

The original OTP is called HOTP (HMAC-based One-Time Password or Hash-based One-Time Password). HOTP is an algorithm published as RFC4226 by the Internet Engineering Task Force (IETF). The moving factor for this type of OTP is a counter.

The token generated is an HMAC hash using a secret key (also known as a seed) and a counter. The output of the hash is pretty long, so it’s shortened to be more suitable as a token. Each token has a counter, and whenever the end-user generates a token, the counter against the token increases. The server also has a counter, but its counter increases when the token is validated.

The key to this type of authentication is the look-ahead window on the server. It’s easy for the server counter and the token counter to be out of sync, so to resynchronize the two counters, the server uses a look-ahead window of size n. If the token counter falls within the window, then it can be validated.

However, there is an issue with this algorithm. If the number of tokens generated exceeds the look-ahead window, you’ll end up locking the system. In other words, you won’t be able to log in. I leave it to your curiosity to dive deeper into that security issue.

TOTP is a time-based one-time password, based on HOTP, which was published as RFC6238 by IETF. Unlike HOTP with a moving factor of a counter, TOTP is an algorithm with a moving factor of time. The generated token is valid for a duration of time, also known as a timestep.

For example, if you open the Google Authenticator app, you’ll notice a timer running against some of your tokens. Once the timer expires, the Authenticator generates a new token.

The speakeasy package supports both algorithms, but in this article, I’ll be focusing on TOTP.

As I previously noted, you can send a token via SMS or email, or you can use an Authenticator app to fetch a token. If you choose the Authenticator app route, generate a QR Code for the end-user to scan on their initial set up of 2FA. Then verify the secret using a token generated by the Authenticator. If you choose the SMS/Email route, create a token to send, and verify against that token.

I want to emphasize all of this should happen on the server! Please don’t pass around secrets on the client-side. It defeats the purpose of security. The client-side should show the QRCode, just once, for the end-user to scan.

Generate A Secret

The speakeasy method generateSecret returns an object with a couple of encodings, such as ASCII, hexadecimal, base32, otpauth_url etc.

import { generateSecret } from 'speakeasy'interface TwoFactorEntity {
userId: number
secret: string
enabled: boolean // default value is false
}const generateUserSecret =
(userRepo: Respository, twoFactor: TwoFactorEntity) => {const secret = generateSecret()
twoFactor.secret = secret.base32
userRepo.save(twoFactor)
return secret}

After generating a secret, store one of the encodings in your database. This storage should be a temporary placeholder or have some indication the account has not enabled 2FA as of yet.

The end-user must complete the set-up (generate a secret, verify the token) to enable 2FA. If they don’t successfully verify the secret on set-up, the secret is useless, and tokens shouldn’t be generated against it because the end-user will never be able to log in.

In my implementation, I created an enabled column on an entity with a relationship to a user, TwoFactorEntity. This flag is true if the end-user successfully verifies against the secret during set-up. This flag should never reset on subsequent verifications. Having this column means I can store the secret just once in my database (i.e., I wouldn’t need a temporary column and a permanent column).

Also note, I returned the secret object; this if for the next step!

Display QR Code

If you’re using the Authenticator app route, read on!

If not, jump to the next header.

Install qrcode

Using the secret object, pass the otpauth_url encoding to the toDataURL method, and it will return a PNG data URL. Display this URL in an IMG tag for the end-user to scan. Remember to generate this data URL on the server and pass the URL back to the client. Don’t pass the secret!

import { toDataURL } from 'qrcode'interface SecretData {
otpauth_url: string
}const generateQRCode = async (secret: SecretData) => {
return await toDataURL(secret.otpauth_url)
}

Generate a Token

If you’re opting for the SMS/Email route, then read on.

If not, jump to the next step.

Generating a TOTP token is simple, but there are a few customizations you might want to make. You can change the algorithm used to create the token, the timestep, the number of digits your token should be, etc. I’ll include the direct link to the documentation for those options here. So ensure you read the speakeasy documentation, and consult your team to figure out what is the right course of action for you. For now, I’ll show you the bare bones of what to do.

To generate a token, use the encoding saved in your database. Use the totpconstructor to pass in the secret, the encoding, and any other options.

import { totp } from 'speakeasy'interface SecretData {
base32: string
}const generateToken = (secret: SecretData) => {
const token = totp({ secret: secret.base32, encoding: 'base32'})
return token
}

Next, use a mail or SMS client to send the token to the end-user. Once the end-user receives the token, verify it. The next step will teach you how to write the verify function.

Verify the Token

The last step! Congratulations, you made it this far. At this point, you should have a method that generates a secret, and some strategy that will create a token for the end-user.

This step relies on the end-user providing the server a token. The token was sent via SMS or Email, or generated in the Authenticator app. Using the token passed to the server and the secret saved in your database, verify the end-user.

Similarly to generating a token, speakeasy provides different options to pass into the verify method. Here is the link to their verify documentation for more options.

import { totp } from 'speakeasy'const verifyToken = (userToken: string, serverSecret: string) => {
const verified = totp.verify({
secret: serverSecret,
encoding: 'base32',
token: userToken
})return verified
}const enableTwoFactor = (
verified: boolean,
repo: Respository,
twoFactor: TwoFactorEntity) => {if (!twoFactor.enabled) {
twoFactor.enabled = verified
}repo.save(twoFactor)
}

In my implementation, I used a different method to fetch my token entity. I didn’t include the retrieval here. The verify method will returns true, if the user token verifies against the secret or falseotherwise. The result of the verification process will help enable 2FA against a user.

TheenableTwoFactor function uses my database entity to enable 2FA, if and only if, two factor wasn’t enabled before, which should only happen once.

There’s evident work to be considered after you verify and before you enable. For example, if a user isn’t verified and the enabled flag is false what should happen? How does that appear to the user? What kind of response should the server return? All of that depends on your application, so I leave it to you to fill in the blanks.

That’s it; you’ve successfully implemented 2FA in three easy steps. The hardest part of integrating 2FA is all the little steps in between, which depend on your application (i.e., setting up endpoints, integrating your email or SMS service, figuring out the front-end portion, etc.).

Once the end-user enables 2FA, for every subsequent login, your server will generate a token, or the end-user will look to the authenticator app, and then enter that token into your application.

I hope this article gave you a good idea of what 2FA is and how to implement it in your application.

Happy Coding!

Upvote


user
Created by

Landy Simpson

University of Toronto Alumni Software Developer and Writer


people
Post

Upvote

Downvote

Comment

Bookmark

Share


Related Articles