DEV Community

Cover image for Add Two-Factor Authentication to Node.js Web Apps
Chen Hui Jing for Vonage

Posted on • Originally published at nexmo.com on

Add Two-Factor Authentication to Node.js Web Apps

Two-factor authentication (2FA) gets its name from the fact that you require two things to verify your identity. Something you know, like a password, and something you have, like the verification code from your mobile device or physical token.

Adding 2FA to your application does not have to be a difficult chore. This tutorial will cover how to implement 2FA for your web applications and services for an additional layer of security with the help of the Nexmo Verify API. We will be building a simple Koa.js application to understand how the underlying mechanism works. This will make it easier to see how this will fit into your own existing projects, even if you are not using Koa.js.

You would start off with a login page that asks your user for a mobile phone number. Upon submission, they would be asked to input a verification code that gets sent to their mobile phone number via SMS. Once that’s sorted, they can access the application.

Prerequisites

  • A basic understanding of Javascript
  • Node.js installed on your machine
  • A Nexmo account (for your API credentials)

This tutorial will take you through the process from scratch. If you’d like to see the finished code, you can clone the git repository for this project. We also have a Glitch version, which has a more over-the-top design, and you can remix it as well. Note that they are slight differences for the Glitch implementation to cater for how projects are hosted on the platform.

Glitch version of demo

Starting a Koa.js project from scratch

Create a project folder on your local machine, then run the following command to set up a new Node.js project.

npm init
Enter fullscreen mode Exit fullscreen mode

This will trigger a series of prompts that will generate your package.json file. You can choose to leave the answers blank to use the default values if you wish.

Configuring package.json

Next, install Koa.js. Do note that Koa requires node v7.6.0 or higher for ES2015 and async function support.

npm install koa --save
Enter fullscreen mode Exit fullscreen mode

Create a server.js file in your project folder.

touch server.js
Enter fullscreen mode Exit fullscreen mode

Paste the following code into your newly created file.

const Koa = require('koa')
const port = process.env.PORT || 3000
const app = new Koa()

app.use(async ctx => {
  ctx.body = 'Hello Unicorn 🦄'
})

const listener = app.listen(port, function() {
  console.log('Your app is listening on port ' + listener.address().port)
})
Enter fullscreen mode Exit fullscreen mode

Run the server.js file.

node server.js
Enter fullscreen mode Exit fullscreen mode

If you navigate to http://localhost:3000 from your browser, you should see an empty page with the text, “Hello Unicorn 🦄”.

Check that server is running

You should also install dotenv, which allows you to load environment variables stored in a .env file into process.env.

npm install dotenv --save
Enter fullscreen mode Exit fullscreen mode

And now you can create the .env file and it should contain at least the following variables:

NEXMO_API_KEY=''
NEXMO_API_SECRET=''
Enter fullscreen mode Exit fullscreen mode

To access environment variables, you’ll have to require it, ideally at the top of your server.js file.

require('dotenv').config()
Enter fullscreen mode Exit fullscreen mode

If you haven’t signed up for a Nexmo account yet, now is a pretty good time to do it. Once you’ve logged into the dashboard, your API credentials should be the first thing you see. Be sure to enclose both your key and secret with quotes.

Project structure

Right now, your project would probably only have a package.json, a server.js file and a .env file. Let’s set up the project structure so you can have a basic frontend for users to interact with.

PROJECT_NAME/               
    |-- public/             
    | |-- client.js
    | `-- style.css
    |-- views/
    | `-- index.html
    |-- .env
    |-- package.json
    `-- server.js
Enter fullscreen mode Exit fullscreen mode

With that, you’ll have to make some tweaks to the server.js file to serve the index.html file and related assets, instead of simply a line of text. Koa.js is a fairly barebones framework, so any additional functionalities for routing or serving static assets need to be installed separately. Here is the list of additional modules and their uses:

  • koa-static for serving static assets
  • koa-bodyparser for handling data sent over via POST requests
  • koa-router for routing
  • koa-views to render templates

This example also makes use of Nunjucks to render template files. The Nexmo Verify API will be used to trigger the verification code via SMS, so you will need to install Nexmo’s Node.js client library as well.

npm install koa-static koa-bodyparser koa-router koa-views nunjucks nexmo --save
Enter fullscreen mode Exit fullscreen mode

Serving static assets and HTML files

To allow the application to serve static assets. like stylesheets and client-side Javascript, out of the /public folder, you can add the following to the server.js file:

const serve = require('koa-static')
app.use(serve('./public'))
Enter fullscreen mode Exit fullscreen mode

To serve HTML files out of the /views folder, you can make use of koa-views, which provides a render() function. The templating engine used in this example is Nunjucks, but you are free to choose whichever templating engine works best for you.

const views = require('koa-views')
app.use(views('./views', { map: { html: 'nunjucks' }}))
Enter fullscreen mode Exit fullscreen mode

The next thing to set up would be some basic routes for serving your application pages.

const Router = require('koa-router')
const router = new Router()

router.get('/', (ctx, next) => {
  return ctx.render('./index')
})

app.use(router.routes()).use(router.allowedMethods())
Enter fullscreen mode Exit fullscreen mode

For this example, you will need 3 pages, the index.html as the main landing page, verify.html for users to input their verification code and result.html to show if the verification was successful or not.

The structure of the web form is fairly straightforward, and you are free to spruce it up with CSS however you wish.

<form method="post" action="verify">
  <input name="phone" type="tel" placeholder="+6588888888">
  <button>Get OTP</button>
</form>
Enter fullscreen mode Exit fullscreen mode

This form will post the user inputs to the /verify route and you can use the phone number in the input to trigger the verification code request. A similar form can be used for the other 2 routes for /check and /cancel as well.

<form method="post" action="check">
  <input name="pin" placeholder="Enter PIN">
  <input name="reqId" type="hidden" value="">
  <button>Verify</button>
</form>


<form method="post" action="cancel">
  <input name="reqId" type="hidden" value="">
  <button class="inline">Cancel verification</button>
</form>
Enter fullscreen mode Exit fullscreen mode

Handling user inputs

Then, for handling user inputs via web forms, you will need some routes to handle POST requests as well. Do make sure to declare bodyparser() before any of the routes.

const bodyParser = require('koa-bodyparser')

/* This should appear before any routes */
app.use(bodyParser())

router.post('/verify/', async (ctx, next) => {
  const payload = await ctx.request.body
  /* Function to trigger verification code here */
})

router.post('/check/', async (ctx, next) => {
  const payload = await ctx.request.body
  /* Function to check verification code here */
})

router.post('/cancel/', async (ctx, next) => {
  const payload = await ctx.request.body
  /* Function to cancel verification code here */
})
Enter fullscreen mode Exit fullscreen mode

Now that you’re able to receive your user’s phone number, you will need to use the Verify API to send a PIN code to it. Initialise a new Nexmo instance with your credentials.

const Nexmo = require('nexmo');
const nexmo = new Nexmo({
  apiKey: YOUR_API_KEY,
  apiSecret: YOUR_API_SECRET
});
Enter fullscreen mode Exit fullscreen mode

There are 3 functions we need to take care of. The first one is to trigger the verification code with the nexmo.verify.request() function. It involves the user’s phone number, and a string for the brand name which will be displayed to the user as the sender.

async function verify(number) {
  return new Promise(function(resolve, reject) {
    nexmo.verify.request({
      number: number,
      brand: process.env.NEXMO_BRAND_NAME
    }, (err, result) => {
      if (err) {
        console.error(err)
        reject(err)
      } else {
        resolve(result)
      }
    })
  })
}
Enter fullscreen mode Exit fullscreen mode

Once your user received the PIN code via SMS, they will have to submit it to the nexmo.verify.check() function, so it can be verified. You will notice a request_id parameter. This value is obtained when the PIN code was successfully triggered. There are a number of ways to pass the request ID into the nexmo.verify.check() function, and this example makes use of a hidden field in the check form.

async function check(reqId, code) {
  return new Promise(function(resolve, reject) {
    nexmo.verify.check({
      request_id: reqId,
      code: code
    }, (err, result) => {
      if (err) {
        console.error(err)
        reject(err)
      } else {
        resolve(result)
      }
    })
  })
}
Enter fullscreen mode Exit fullscreen mode

The last function gives your user the option to cancel the verification if they changed their mind. It uses the nexmo.verify.control() function, and again, requires the request ID generated from triggering the PIN code and a string value of cancel.

async function cancel(reqId) {
  return new Promise(function(resolve, reject) {
    nexmo.verify.control({
      request_id: reqId,
      cmd: 'cancel'
    }, (err, result) => {
      if (err) {
        console.error(err)
        reject(err)
      } else {
        resolve(result)
      }
    })
  })
}
Enter fullscreen mode Exit fullscreen mode

Landing page for demo

Now you need to make use of these 3 functions in the routes we specified earlier, starting with the one for triggering the verification code first.

router.post('/verify/', async (ctx, next) => {
  const payload = await ctx.request.body
  const phone = payload.phone

  const result = await verify(phone)
  const reqId = result.request_id 
  ctx.status = 200
  return ctx.render('./verify', { reqId: reqId })
})
Enter fullscreen mode Exit fullscreen mode

The ctx.request.body will look something like this:

{ phone: '+40987654321' }
Enter fullscreen mode Exit fullscreen mode

You can grab that phone number and pass it to the verify() function. As long as it is a valid phone number, the verification code will be fired off and you will receive a response containing a request_id and status.

{ 
  request_id: '1bf002ecd1e94d8aa81ba7463b19f583',
  status: '0'
}
Enter fullscreen mode Exit fullscreen mode

From there, you can send the request ID over to the frontend for use when the user enters the verification code.

The request_id is passed to the frontend

When your user submits the correct PIN, you will need to plug both the PIN and the request ID into the check() function.

router.post('/check/', async (ctx, next) => {
  const payload = await ctx.request.body
  const code = payload.pin
  const reqId = payload.reqId

  const result = await check(reqId, code)
  const status = result.status
  ctx.status = 200
  return ctx.render('./result', { status: status })
})
Enter fullscreen mode Exit fullscreen mode

Again, both those values can be obtained from the ctx.request.body and if the PIN is validated to be correct, you will receive a response that looks like this:

{ request_id: '1bf002ecd1e94d8aa81ba7463b19f583',
  status: '0',
  event_id: '150000001AC57AB2',
  price: '0.10000000',
  currency: 'EUR' }
Enter fullscreen mode Exit fullscreen mode

You can then make use of the status code to determine what message you would like to display to your user. This example uses Nunjucks, so the markup on the results page could look something like this:

{% if status == 0 %}
<p>Code verified successfully. ¯\_(ツ)_/¯</p>
{% else %}
<p>Something went wrong… ಠ_ಠ</p>
<p>Please contact the administrator for more information.</p>
{% endif %}
Enter fullscreen mode Exit fullscreen mode

Verification messages

This was a thorough break-down of each part of the code but for a look at how the application looks like in its entirety, do check out the source code on GitHub.

Additional things to take care of

This tutorial is a stripped down version, highlighting only the bits necessary for implementing two-factor authentication. But there are numerous things that have to be taken care of in an actual application. One of the most important is error handling. The Verify API returns a status value of 0 for successful queries, but any other value indicates an error.

These errors should be handled and the user interface on the frontend should reflect any potential errors preventing sucessful verification. It might also be a good idea to implement some sort of frontend validation, or even utilise Nexmo’s Number Insight API to ensure only valid phone numbers are passed to the Verify API.

Where Next?

If you are keen to do more with these APIs, here are some links that might be helpful to you:

Top comments (0)