Implementing 2 Factor Authentication using Authy

gathoni

Mary Gathoni

Posted on June 2, 2020

Implementing 2 Factor Authentication using Authy

Hey there 👋

So what is 2FA?

The traditional way of user authentication involved the user sending an email and password which are then verified after which the user is logged in. This brings about a set of potential risks such as hacking, weak passwords, passwords reuse and many more.
2 factor authentication provides an extra step that the user has to complete before being authenticated.
This article aims at showing you how to use Authy from Twilio in your node API so as to implement 2FA.
We will be sending a code to the user's cell during sign-in which they have to send back to the API for verification.

Having said that, let's dive in

Find the completed source code of this application here

Requirements

For this project, you'll have to be know a little bit of JavaScript and working with node
Have node and npm in you machine. If not you can get it here

Get an API key from the Authy dashboard.

Head over to the Authy dashboard and sign-in either with a Twilio account or using Authy credentials.
After entering your Authy token you'll be redirected to your dashboard where you can create a new application.

  • Click on the New Application button at the bottom of the navigation menu
  • Enter your preferred app name in the pop-up dialog window and hit create
  • Grab the API key for production, we'll use it later. Alt Text

Create a folder and name it according to you preference, move into it and in a terminal initialize npm

npm init
Enter fullscreen mode Exit fullscreen mode

This will generate a package.json file for you.

Install dependencies

npm i express, mongoose,bcryptjs, authy, dotenv
Enter fullscreen mode Exit fullscreen mode

Create a .env file and add the port number.

Create routes

In route.js,

const router = require('express').Router();
router.get( '/ping', async (req, res) => {
  res.send('pong')
});
module.exports = router;
Enter fullscreen mode Exit fullscreen mode

This is a route to test if our app is working

Start server

In app.js

const express = require('express');
require('dotenv').config();
const routes = require('./routes');

const app = express();

app.use(express.json());
app.use('/user', routes)

const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
    console.log(`Listening on ${PORT}`)
});
Enter fullscreen mode Exit fullscreen mode

To run the app:

node app
Enter fullscreen mode Exit fullscreen mode

You should see the following in the terminal:
Alt Text
Testing this route:
Alt Text

Now that we're sure the app works just fine, we can continue

Connect database

In db.js, add the following code
We're using a database so that we can work with persistent data.

const mongoose = require('mongoose');
require('dotenv').config();

mongoose.connect(process.env.MONGO_URI, {useNewUrlParser: true, useUnifiedTopology: true});

const db = mongoose.connection;

db.once('open', () => {
    console.log('MMongoose default connection open');
});

db.on('error', () => {
    console.log('An error occurred while connectiong to database');
});
Enter fullscreen mode Exit fullscreen mode

To connect the database, you will have to import db.js in app.js just after importing your other packages

Define the user model

In model.js, add the following code

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const { Schema } = mongoose;

const userSchema = new Schema({
    email: {
        type: String,
        required: true
    },
    password: {
        type: String,
        required: true
    },
    authyId: {
        type: String,
        default: null
    }
});

userSchema.pre('save', async function(next) {
    try {
        if (!this.isModified('password')) next();
        const salt = await bcrypt.genSalt(10);
        const hashedPassword = await bcrypt.hash(this.password, salt);
        this.password = hashedPassword;
        next();
    } catch (error) {
        return next(error);
    }
});

userSchema.methods.comparePassword = function(plainPwd) {
return bcrypt.compareSync(plainPwd, this.password);
};

module.exports = mongoose.model('User', userSchema);
Enter fullscreen mode Exit fullscreen mode

You may have noticed the authyId value, this is to store the id sent back by Authy after the user is registered. This id is shared across all sites that use that phone number, convenient right?

Create user account: sign up route

Add this code to route.js

router.post( '/signup', async (req, res) => {
    try {
        const { email, password } = req.body;
        const exists = await User.findOne({ email });
        if (exists) {
            return res.json({ message: "Email exists"});
        }
        const newUser = new User({
            email,
            password
        });
        await newUser.save();
        res.status(200).json({ message: "User account registered"})
    } catch (error) {
        res.status(500).json({ message: error.message });
    }
});
Enter fullscreen mode Exit fullscreen mode

The above code creates a user using a unique email and a password.
Alt Text

Enable 2 factor authentication

Before adding code to enable 2fa, we'll have to initialize Authy first.

Insert the following code above your routes code

require('dotenv').config();
const authy = require('authy')(process.env.AUTHY_API_KEY);
Enter fullscreen mode Exit fullscreen mode

Then create the route

router.post('/enable/2fa', async (req, res) => {
    try {
        const { email, countryCode, phone } = req.body;
        const user = await User.findOne({ email });
        if (!user) {   
            return res.json({ message: 'User account does not exist' });
        }
        authy.register_user(email, phone, countryCode, (err, regRes) =>{
            if (err) {
                return res.json({ message: 'An error occurred while registering user' });
            }
            user.authyId = regRes.user.id;
            user.save((err, user) => {
                if (err) {
                    return res.json({ message: 'An error occurred while saving authyId into db' });
                }
            })
        });
        res.status(200).json({ message: '2FA enabled' });
    } catch (error) {
        res.status(500).json({ message: error.message });
    }
});

Enter fullscreen mode Exit fullscreen mode

Let's take a closer look at the above code. To register a user Authy requires the email, phone number and country code of the user.
First we have to confirm if that user account exists and if doesn't we'll send an error message.
Then call authy.register_user passing the user's credentials, this function returns a registered user whose id we store in the database.
Alt Text

Sign in route

For this route, we want to be able to handle a sign in using only the email and password, for users who haven't enabled 2fa and also enforce 2fa sign in for those who've enabled it.
To check whether the user has enabled 2fa we'll check if their authyId from the database is set to null, if it is, we'll log them in using an email and password only and if otherwise send an OTP to their cell.

Add the following route in route.js

router.post('/signin', async (req, res) => {
    try {
        const { email, password } = req.body;
        const user = await User.findOne({ email });
        const isMatch = await user.comparePassword(password);
        if (!user || !isMatch) {
            return res.json({ message: "Email or password incorrect"});
        }
        if (user.authyId) {
            authy.request_sms(
                user.authyId, {force: true},  
                function (err, smsRes) {
                    if (err) {
                        return res.json({ message: 'An error occurred while sending OTP to user' });
                    } 
            });
            return res.status(200).json({ message: 'OTP sent to user' });
        }
        res.status(200).json({ message: "Logged in successfully"})
    } catch (error) {
        res.status(500).json({ message: error.message })
    }
});

Enter fullscreen mode Exit fullscreen mode

If user has not enabled 2fa:
Alt Text
If user has enabled 2fa:
Alt Text

Verification route

To verify the OTP sent to the user's cell, we call authy's verify function and pass to it the user id and the OTP.
If no errors occur, the user has now authenticated with a second factor and is logged in.

router.post('/verify/:token', async (req, res) => {
    try {
        const { email } = req.body
        const user = await User.findOne({ email });
        authy.verify(
            user.authyId,
            req.params.token,
            function(err, tokenRes){
                if (err) {
                    res.json({ message: 'OTP verification failed'});
                }
                res.status(200).json({ message: 'Token is valid'});
            });
    } catch (error) {
        res.status(500).json({ message: error.message});
    }
});
Enter fullscreen mode Exit fullscreen mode

Alt Text

To recap, the main key things are

  • Get an API key from Authy's dashboard.
  • Register a user uswing authy's register_user function.
  • Request an sms to be sent to the cell of the registered user.
  • Verify the code received by the user.

You have just implemented 2 Factor Authentication using Node.js and Authy.👏

Find the completed source code of this application here

💖 💪 🙅 🚩
gathoni
Mary Gathoni

Posted on June 2, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related