Mary Gathoni
Posted on June 2, 2020
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.
Create a folder and name it according to you preference, move into it and in a terminal initialize npm
npm init
This will generate a package.json file for you.
Install dependencies
npm i express, mongoose,bcryptjs, authy, dotenv
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;
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}`)
});
To run the app:
node app
You should see the following in the terminal:
Testing this route:
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');
});
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);
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 });
}
});
The above code creates a user using a unique email and a password.
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);
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 });
}
});
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.
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 })
}
});
If user has not enabled 2fa:
If user has enabled 2fa:
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});
}
});
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
Posted on June 2, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.