Back

Tutorials

Jul 25, 2023

How to Implement Two Factor Authentication in Node.js REST API

Anand Sukumaran

With many authentication techniques, two-factor authentication is one of the most user-friendly and secure methods as it is becoming the industry standard. In this article, we will see how you can implement two-factor authentication (2FA) in your own app or REST API built in NodeJS.

What is Two-Factor Authentication?

You might have seen this in many of the modern applications but what exactly it is? This authentication method requires more than one form of user authentication identifier and simple old traditional password does not work. A user is required to set up the biometrics, or proceed with a code emailed or texted two him.

Getting started

Before you continue, you will need nodejs and npm installed on your local machine with a code editor of your choice. Node.js is one of the popular frameworks with lot of frameworks and libraries like Express, nodemailer etc to build a web application.You can download nodejs from this link. The next step is to create a project directory and enter the following command inside it.

npm init -y

This will generate package.json file for you. Now run the following command to install all the dependencies that we will be using in our project.

npm install express node-json-db uuid speakeasy

We will be using express js which is lightweight framework to run the server. Along with this, node-json-db is our database for the application and finally, for the two-factor authentication, we are using speakeasy which is a one-time password generator. Create a new file named server.js and add the following code to it.

const express = require('express');
const JsonDB = require('node-json-db').JsonDB;
const Config = require('node-json-db/dist/lib/JsonDBConfig').Config;
const uuid = require('uuid');
const speakeasy = require('speakeasy');
const app = express();
const configOfDb = new Config('myDb', true, false, '/');
const myDb = new JsonDB(configOfDb);
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.get('/api', (req, res) => {
 res.json({ message: 'Two factor auth!' });
});
const port = 9000;
app.listen(port, () => {
 console.log(`App is running on PORT: ${port}.`);
});

Run node server.js command to start your development server. If you see the message App is running on PORT: 9000, that means your app is live and you can access it using http://localhost:9000

Generating the Secret Key

Now, we create the secret key that generates the two-factor authentication code to be used with the authenticator extension. The secret will be temporary unless it had been verified by us that it was generated by google authenticator using the provided secret key. Let us create a route that will create a user and secret key by speakeasy.

app.post('/api/register', (req, res) => {
    const id = uuid.v4();
    try {
      const path = `/user/${id}`;
      
      const temp_secret = speakeasy.generateSecret();
      
      myDb.push(path, { id, temp_secret });
      
      res.json({ id, secret: temp_secret.base32 });
    } catch (e) {
      console.log(e);
      res.status(500).json({ message: 'Error generating secret key' });
    }
   });

When we send a POST request to this endpoint, we will receive the secret key.

{
  "id":"74748hd-shsc92yd-48r8n-28dcck",
  "secret":"NALDICNBCGWJ3S9KVG026S40OR8QRB7VP5UR34SVMLYWWN"
}

We are using the Google Authenticator chrome extension and can register our secret as follows.

Verifying the secret

Till now, we have assigned the same secret to the user as well as the google authenticator. The authenticator will generate a code based on the secret and we will verify this in our backend against the registered user id. Let us create another route for this process.

app.post('/api/verify', (req, res) => {
    const { userId, token } = req.body;
    try {
      const path = `/user/${userId}`;
      console.log({ user });
      const user = db.getData(path);
      const { base32: secret } = user.temp_secret;
      const verified = speakeasy.totp.verify({
        secret,
        encoding: base32,
        token,
      });
      if (verified) {
        myDb.push(path, { id: userId, secret: user.temp_secret });
        res.json({ verified: true });
      } else {
        res.json({ verified: false });
      }
    } catch (error) {
      console.error(error);
      res.status(500).json({ message: 'Error retrieving user' });
    }
   });

With the token obtained from Google Authenticator, this request will verify the request and returns the following.

{
  "verified":true
}

Since our token is verified, it will be stored permanently and will be used to verify future codes. For that, we will create a validation endpoint.

app.post('/api/validate', (req, res) => {
 const { userId, token } = req.body;
 try {
   const path = `/user/${userId}`;
   const user = myDb.getData(path);
   const { base32: secret } = user.secret;
   const tokenValidates = speakeasy.totp.verify({
     secret,
     encoding: 'base32',
     token,
     window: 1,
   });
   if (tokenValidates) {
     res.json({ validated: true });
   } else {
     res.json({ validated: false });
   }
 } catch (error) {
   console.error(error);
   res.status(500).json({ message: 'Error retrieving user' });
 }
});

With this we can validate as my codes from authenticator app as we like!

Final Code

So, with few lines of code and without any third-party APIs, we've implemented Two Factor Authentication in our Node.js application. The full server.js file will look as follows.

const express = require('express');
const JsonDB = require('node-json-db').JsonDB;
const Config = require('node-json-db/dist/lib/JsonDBConfig').Config;
const uuid = require('uuid');
const speakeasy = require('speakeasy');
const app = express();
const configOfDb = new Config('myDb', true, false, '/');
const myDb = new JsonDB(configOfDb);
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.get('/api', (req, res) => {
 res.json({ message: 'Two factor auth!' });
});
const port = 9000;
app.listen(port, () => {
 console.log(`App is running on PORT: ${port}.`);
});
app.post('/api/register', (req, res) => {
 const id = uuid.v4();
 try {
   const path = `/user/${id}`;
   const temp_secret = speakeasy.generateSecret();
   // Create user in the database
   myDb.push(path, { id, temp_secret });
   res.json({ id, secret: temp_secret.base32 });
 } catch (e) {
   console.log(e);
   res.status(500).json({ message: 'Error generating secret key' });
 }
});
app.post('/api/verify', (req, res) => {
 const { userId, token } = req.body;
 try {
   const path = `/user/${userId}`;
   console.log({ user });
      const user = myDb.getData(path);
   const { base32: secret } = user.temp_secret;
   const verified = speakeasy.totp.verify({
     secret,
     encoding: base32,
     token,
   });
   if (verified) {
     myDb.push(path, { id: userId, secret: user.temp_secret });
     res.json({ verified: true });
   } else {
     res.json({ verified: false });
   }
 } catch (error) {
   console.error(error);
   res.status(500).json({ message: 'Error retrieving user' });
 }
});
app.post('/api/validate', (req, res) => {
 const { userId, token } = req.body;
 try {
   const path = `/user/${userId}`;
   const user = myDb.getData(path);
   const { base32: secret } = user.secret;
   console.log({ user });
   const tokenValidates = speakeasy.totp.verify({
     secret,
     encoding: 'base32',
     token,
     window: 1,
   });
   if (tokenValidates) {
     res.json({ validated: true });
   } else {
     res.json({ validated: false });
   }
 } catch (error) {
   console.error(error);
   res.status(500).json({ message: 'Error retrieving user' });
 }
});

Congrats! You have successfully implemented two-factor authentication (2FA) with nodejs and speakeasy!

Anand Sukumaran

Share this post