Skip to contentSkip to navigationSkip to topbar
Rate this page:
On this page

Account Verification with Authy, Node.js and Express


(warning)

Warning

As of November 2022, Twilio no longer provides support for Authy SMS/Voice-only customers. Customers who were also using Authy TOTP or Push prior to March 1, 2023 are still supported. The Authy API is now closed to new customers and will be fully deprecated in the future.

For new development, we encourage you to use the Verify v2 API.

Existing customers will not be impacted at this time until Authy API has reached End of Life. For more information about migration, see Migrating from Authy to Verify for SMS(link takes you to an external page).

Ready to implement user account verification in your application? Here's how it works at a high level:

  1. The user begins the registration process by entering their data, including a phone number, into a signup form.
  2. The authentication system sends a one-time password to the user's mobile phone to verify the phone number.
  3. The user enters the one-time password into a form to complete registration.
  4. The user sees a success page and receives an SMS indicating that their account has been created.

Building Blocks

building-blocks page anchor

To get this done, you'll be working with the following Twilio-powered APIs:

Authy REST API

Twilio REST API

  • Messages Resource : We will use Twilio directly to send our user a confirmation message after they create an account.

Let's get started!


Our first order of business is to create a model object for a user of our application. We will borrow a lot of the code from the User model in the 2FA tutorial that uses Authy as well. This application uses MongoDB(link takes you to an external page) for persistence, but in our code we will primarily interface with Mongoose(link takes you to an external page), a higher-level object modeling tool which is backed by MongoDB.

You'll notice an authyId property on the model - this is required to support integration with the Authy API. We won't use this property right away but we'll need it later for the Authy integration.

One of the properties on the User model is the password. It is not in scope for this tutorial, but take note: you'll probably want it later for logging in a returning user.

User Model definitions for use with Twilio and Authy

user-model-definitions-for-use-with-twilio-and-authy page anchor

models/User.js


_128
const mongoose = require('mongoose');
_128
const bcrypt = require('bcrypt');
_128
const config = require('../config');
_128
_128
// Create authenticated Authy and Twilio API clients
_128
const authy = require('authy')(config.authyKey);
_128
const twilioClient = require('twilio')(config.accountSid, config.authToken);
_128
_128
// Used to generate password hash
_128
const SALT_WORK_FACTOR = 10;
_128
_128
// Define user model schema
_128
const UserSchema = new mongoose.Schema({
_128
fullName: {
_128
type: String,
_128
required: true,
_128
},
_128
countryCode: {
_128
type: String,
_128
required: true,
_128
},
_128
phone: {
_128
type: String,
_128
required: true,
_128
},
_128
verified: {
_128
type: Boolean,
_128
default: false,
_128
},
_128
authyId: String,
_128
email: {
_128
type: String,
_128
required: true,
_128
unique: true,
_128
},
_128
password: {
_128
type: String,
_128
required: true,
_128
},
_128
});
_128
_128
// Middleware executed before save - hash the user's password
_128
UserSchema.pre('save', function(next) {
_128
const self = this;
_128
_128
// only hash the password if it has been modified (or is new)
_128
if (!self.isModified('password')) return next();
_128
_128
// generate a salt
_128
bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) {
_128
if (err) return next(err);
_128
_128
// hash the password using our new salt
_128
bcrypt.hash(self.password, salt, function(err, hash) {
_128
if (err) return next(err);
_128
_128
// override the cleartext password with the hashed one
_128
self.password = hash;
_128
next();
_128
});
_128
});
_128
});
_128
_128
// Test candidate password
_128
UserSchema.methods.comparePassword = function(candidatePassword, cb) {
_128
const self = this;
_128
bcrypt.compare(candidatePassword, self.password, function(err, isMatch) {
_128
if (err) return cb(err);
_128
cb(null, isMatch);
_128
});
_128
};
_128
_128
// Send a verification token to this user
_128
UserSchema.methods.sendAuthyToken = function(cb) {
_128
var self = this;
_128
_128
if (!self.authyId) {
_128
// Register this user if it's a new user
_128
authy.register_user(self.email, self.phone, self.countryCode,
_128
function(err, response) {
_128
if (err || !response.user) return cb.call(self, err);
_128
self.authyId = response.user.id;
_128
self.save(function(err, doc) {
_128
if (err || !doc) return cb.call(self, err);
_128
self = doc;
_128
sendToken();
_128
});
_128
});
_128
} else {
_128
// Otherwise send token to a known user
_128
sendToken();
_128
}
_128
_128
// With a valid Authy ID, send the 2FA token for this user
_128
function sendToken() {
_128
authy.request_sms(self.authyId, true, function(err, response) {
_128
cb.call(self, err);
_128
});
_128
}
_128
};
_128
_128
// Test a 2FA token
_128
UserSchema.methods.verifyAuthyToken = function(otp, cb) {
_128
const self = this;
_128
authy.verify(self.authyId, otp, function(err, response) {
_128
cb.call(self, err, response);
_128
});
_128
};
_128
_128
// Send a text message via twilio to this user
_128
UserSchema.methods.sendMessage =
_128
function(message, successCallback, errorCallback) {
_128
const self = this;
_128
const toNumber = `+${self.countryCode}${self.phone}`;
_128
_128
twilioClient.messages.create({
_128
to: toNumber,
_128
from: config.twilioNumber,
_128
body: message,
_128
}).then(function() {
_128
successCallback();
_128
}).catch(function(err) {
_128
errorCallback(err);
_128
});
_128
};
_128
_128
// Export user model
_128
module.exports = mongoose.model('User', UserSchema);

Now that you've created your user model, let's check out the form template for creating a user.


The New User Form Template

the-new-user-form-template page anchor

When we create a new user, we ask for a name, email address, password and mobile number including country code. In order to validate the user account we use Authy to send a one-time password via SMS to this phone number.

Form template for user creation

form-template-for-user-creation page anchor

views/users/create.jade


_41
extends ../layout
_41
_41
block styles
_41
link(rel='stylesheet', media='screen'
_41
href='//www.authy.com/form.authy.min.css')
_41
_41
block content
_41
h1 We're Going To Be *BEST* Friends
_41
p.
_41
Thank you for your interest in signing up! Can you tell us a bit about
_41
yourself?
_41
_41
form(action='/users', method='POST')
_41
.form-group
_41
label(for='fullName') Your Full Name:
_41
input.form-control(type='text', name='fullName',
_41
placeholder='Peggy Carter')
_41
_41
.form-group
_41
label(for='email') Your email Address:
_41
input.form-control(type='email', name='email',
_41
placeholder='pcarter@ssr.gov')
_41
_41
.form-group
_41
label(for='password') Your Password:
_41
input.form-control(type='password', name='password')
_41
_41
.form-group
_41
label(for='countryCode') Country Code:
_41
select(id='authy-countries', name='countryCode')
_41
_41
.form-group
_41
label(for='phone') Mobile Phone:
_41
input.form-control(type='text', name='phone', placeholder='651-867-5309')
_41
_41
button.btn.btn-primary(type='submit') Create Account
_41
_41
block scripts
_41
// Include Authy form helpers and additional helper script
_41
script(src='//www.authy.com/form.authy.min.js')
_41
script(src='/js/create_form.js')

Now the user is logged in but not verified. In the next steps we'll learn how to verify the user using Authy.


In config.js, we list configuration parameters for the application. Most are pulled in from system environment variables, which is a helpful way to access sensitive values (like API keys). This prevents us from accidentally checking them in to source control.

Now, we need our Authy production key (sign up for Authy here(link takes you to an external page)). Once you create an Authy application, the production key is found on the dashboard:

Authy dashboard.

Configure your application to work with Authy

configure-your-application-to-work-with-authy page anchor

config.js


_45
const dotenvSafe = require('dotenv-safe');
_45
_45
const nodeEnv = process.env.NODE_ENV;
_45
if(nodeEnv && nodeEnv === 'production') {
_45
// If it's running in Heroku, we set MONGO_URL to an arbitrary value so that
_45
// dotenv-safe doesn't throw an error. MONGO_URL is not read in Heroku as
_45
// MONGODB_URI will be set
_45
process.env.MONGO_URL = 'placeholder';
_45
}
_45
dotenvSafe.load();
_45
_45
const cfg = {};
_45
_45
// HTTP Port to run our web application
_45
cfg.port = process.env.PORT || 3000;
_45
_45
// A random string that will help generate secure one-time passwords and
_45
// HTTP sessions
_45
cfg.secret = process.env.APP_SECRET || 'keyboard cat';
_45
_45
// Your Twilio account SID and auth token, both found at:
_45
// https://www.twilio.com/user/account
_45
//
_45
// A good practice is to store these string values as system environment
_45
// variables, and load them from there as we are doing below. Alternately,
_45
// you could hard code these values here as strings.
_45
cfg.accountSid = process.env.TWILIO_ACCOUNT_SID;
_45
cfg.authToken = process.env.TWILIO_AUTH_TOKEN;
_45
_45
// A Twilio number you control - choose one from:
_45
// https://www.twilio.com/user/account/phone-numbers/incoming
_45
// Specify in E.164 format, e.g. "+16519998877"
_45
cfg.twilioNumber = process.env.TWILIO_NUMBER;
_45
_45
// Your Authy production key - this can be found on the dashboard for your
_45
// Authy application
_45
cfg.authyKey = process.env.AUTHY_API_KEY;
_45
_45
// MongoDB connection string - MONGO_URL is for local dev,
_45
// MONGODB_URI is for the MongoLab add-on for Heroku deployment
_45
// when using docker-compose
_45
cfg.mongoUrl = process.env.MONGODB_URI || process.env.MONGO_URL;
_45
_45
// Export configuration object
_45
module.exports = cfg;

Next, we need to jump over to the User model to configure the Authy client and create an instance method to send a one-time password.


Sending a Verification Token

sending-a-verification-token page anchor

When it comes time to actually send the user a verification code, we do that in a User model function.

Before sending the code, an Authy user needs to exist and correlate to our User model in the database. If the authyId for our user instance hasn't been set, we use the Authy API client to create an associated Authy user and store that ID.

Once the user has an authyId, we can send a verification code to that user's mobile phone using the Authy API client(link takes you to an external page).

Check the user's authyId, register a new user, and send a one-time token

check-the-users-authyid-register-a-new-user-and-send-a-one-time-token page anchor

models/User.js


_128
const mongoose = require('mongoose');
_128
const bcrypt = require('bcrypt');
_128
const config = require('../config');
_128
_128
// Create authenticated Authy and Twilio API clients
_128
const authy = require('authy')(config.authyKey);
_128
const twilioClient = require('twilio')(config.accountSid, config.authToken);
_128
_128
// Used to generate password hash
_128
const SALT_WORK_FACTOR = 10;
_128
_128
// Define user model schema
_128
const UserSchema = new mongoose.Schema({
_128
fullName: {
_128
type: String,
_128
required: true,
_128
},
_128
countryCode: {
_128
type: String,
_128
required: true,
_128
},
_128
phone: {
_128
type: String,
_128
required: true,
_128
},
_128
verified: {
_128
type: Boolean,
_128
default: false,
_128
},
_128
authyId: String,
_128
email: {
_128
type: String,
_128
required: true,
_128
unique: true,
_128
},
_128
password: {
_128
type: String,
_128
required: true,
_128
},
_128
});
_128
_128
// Middleware executed before save - hash the user's password
_128
UserSchema.pre('save', function(next) {
_128
const self = this;
_128
_128
// only hash the password if it has been modified (or is new)
_128
if (!self.isModified('password')) return next();
_128
_128
// generate a salt
_128
bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) {
_128
if (err) return next(err);
_128
_128
// hash the password using our new salt
_128
bcrypt.hash(self.password, salt, function(err, hash) {
_128
if (err) return next(err);
_128
_128
// override the cleartext password with the hashed one
_128
self.password = hash;
_128
next();
_128
});
_128
});
_128
});
_128
_128
// Test candidate password
_128
UserSchema.methods.comparePassword = function(candidatePassword, cb) {
_128
const self = this;
_128
bcrypt.compare(candidatePassword, self.password, function(err, isMatch) {
_128
if (err) return cb(err);
_128
cb(null, isMatch);
_128
});
_128
};
_128
_128
// Send a verification token to this user
_128
UserSchema.methods.sendAuthyToken = function(cb) {
_128
var self = this;
_128
_128
if (!self.authyId) {
_128
// Register this user if it's a new user
_128
authy.register_user(self.email, self.phone, self.countryCode,
_128
function(err, response) {
_128
if (err || !response.user) return cb.call(self, err);
_128
self.authyId = response.user.id;
_128
self.save(function(err, doc) {
_128
if (err || !doc) return cb.call(self, err);
_128
self = doc;
_128
sendToken();
_128
});
_128
});
_128
} else {
_128
// Otherwise send token to a known user
_128
sendToken();
_128
}
_128
_128
// With a valid Authy ID, send the 2FA token for this user
_128
function sendToken() {
_128
authy.request_sms(self.authyId, true, function(err, response) {
_128
cb.call(self, err);
_128
});
_128
}
_128
};
_128
_128
// Test a 2FA token
_128
UserSchema.methods.verifyAuthyToken = function(otp, cb) {
_128
const self = this;
_128
authy.verify(self.authyId, otp, function(err, response) {
_128
cb.call(self, err, response);
_128
});
_128
};
_128
_128
// Send a text message via twilio to this user
_128
UserSchema.methods.sendMessage =
_128
function(message, successCallback, errorCallback) {
_128
const self = this;
_128
const toNumber = `+${self.countryCode}${self.phone}`;
_128
_128
twilioClient.messages.create({
_128
to: toNumber,
_128
from: config.twilioNumber,
_128
body: message,
_128
}).then(function() {
_128
successCallback();
_128
}).catch(function(err) {
_128
errorCallback(err);
_128
});
_128
};
_128
_128
// Export user model
_128
module.exports = mongoose.model('User', UserSchema);

After the user receives the verification code, they will pass it to the application using this form(link takes you to an external page).

Let's check out the controller that handles the form.


Verifying the Code: Controller

verifying-the-code-controller page anchor

This controller function handles the form's submission. It's a little longer than the others, but it has a lot to do. It needs to:

  • Load a User model for the current verification request.
  • Use an instance function on the model object to verify the code that was entered by the user.
  • If the code entered was valid, it will flip a boolean flag on the user model to indicate the account was verified.

Take a look at the User model to see the instance method that handles verifying the code with Authy.

Handle submission of a verification token

handle-submission-of-a-verification-token page anchor

controllers/users.js


_164
const User = require('../models/User');
_164
_164
// Display a form that allows users to sign up for a new account
_164
exports.showCreate = function(request, response) {
_164
response.render('users/create', {
_164
title: 'Create User Account',
_164
// include any errors (success messages not possible for view)
_164
errors: request.flash('errors'),
_164
});
_164
};
_164
_164
// create a new user based on the form submission
_164
exports.create = function(request, response) {
_164
const params = request.body;
_164
_164
// Create a new user based on form parameters
_164
const user = new User({
_164
fullName: params.fullName,
_164
email: params.email,
_164
phone: params.phone,
_164
countryCode: params.countryCode,
_164
password: params.password,
_164
});
_164
_164
user.save(function(err, doc) {
_164
if (err) {
_164
// To improve on this example, you should include a better
_164
// error message, especially around form field validation. But
_164
// for now, just indicate that the save operation failed
_164
request.flash('errors', 'There was a problem creating your'
_164
+ ' account - note that all fields are required. Please'
_164
+ ' double-check your input and try again.');
_164
_164
response.redirect('/users/new');
_164
} else {
_164
// If the user is created successfully, send them an account
_164
// verification token
_164
user.sendAuthyToken(function(err) {
_164
if (err) {
_164
request.flash('errors', 'There was a problem sending '
_164
+ 'your token - sorry :(');
_164
}
_164
_164
// Send to token verification page
_164
response.redirect('/users/'+doc._id+'/verify');
_164
});
_164
}
_164
});
_164
};
_164
_164
// Display a form that allows users to enter a verification token
_164
exports.showVerify = function(request, response) {
_164
response.render('users/verify', {
_164
title: 'Verify Phone Number',
_164
// include any errors
_164
errors: request.flash('errors'),
_164
// success messsages
_164
successes: request.flash('successes'),
_164
// Include database ID to include in form POST action
_164
id: request.params.id,
_164
});
_164
};
_164
_164
// Resend a code if it was not received
_164
exports.resend = function(request, response) {
_164
// Load user model
_164
User.findById(request.params.id, function(err, user) {
_164
if (err || !user) {
_164
return die('User not found for this ID.');
_164
}
_164
_164
// If we find the user, let's send them a new code
_164
user.sendAuthyToken(postSend);
_164
});
_164
_164
// Handle send code response
_164
function postSend(err) {
_164
if (err) {
_164
return die('There was a problem sending you the code - please '
_164
+ 'retry.');
_164
}
_164
_164
request.flash('successes', 'Code re-sent!');
_164
response.redirect('/users/'+request.params.id+'/verify');
_164
}
_164
_164
// respond with an error
_164
function die(message) {
_164
request.flash('errors', message);
_164
response.redirect('/users/'+request.params.id+'/verify');
_164
}
_164
};
_164
_164
// Handle submission of verification token
_164
exports.verify = function(request, response) {
_164
let user = {};
_164
_164
// Load user model
_164
User.findById(request.params.id, function(err, doc) {
_164
if (err || !doc) {
_164
return die('User not found for this ID.');
_164
}
_164
_164
// If we find the user, let's validate the token they entered
_164
user = doc;
_164
user.verifyAuthyToken(request.body.code, postVerify);
_164
});
_164
_164
// Handle verification response
_164
function postVerify(err) {
_164
if (err) {
_164
return die('The token you entered was invalid - please retry.');
_164
}
_164
_164
// If the token was valid, flip the bit to validate the user account
_164
user.verified = true;
_164
user.save(postSave);
_164
}
_164
_164
// after we save the user, handle sending a confirmation
_164
function postSave(err) {
_164
if (err) {
_164
return die('There was a problem validating your account '
_164
+ '- please enter your token again.');
_164
}
_164
_164
// Send confirmation text message
_164
const message = 'You did it! Signup complete :)';
_164
user.sendMessage(message, function() {
_164
// show success page
_164
request.flash('successes', message);
_164
response.redirect(`/users/${user._id}`);
_164
}, function(err) {
_164
request.flash('errors', 'You are signed up, but '
_164
+ 'we could not send you a message. Our bad :(');
_164
});
_164
}
_164
_164
// respond with an error
_164
function die(message) {
_164
request.flash('errors', message);
_164
response.redirect('/users/'+request.params.id+'/verify');
_164
}
_164
};
_164
_164
// Show details about the user
_164
exports.showUser = function(request, response, next) {
_164
// Load user model
_164
User.findById(request.params.id, function(err, user) {
_164
if (err || !user) {
_164
// 404
_164
return next();
_164
}
_164
_164
response.render('users/show', {
_164
title: 'Hi there ' + user.fullName + '!',
_164
user: user,
_164
// any errors
_164
errors: request.flash('errors'),
_164
// any success messages
_164
successes: request.flash('successes'),
_164
});
_164
});
_164
};

Now let's see how we can use Authy to actually verify the code.


Verifying the Code: Model

verifying-the-code-model page anchor

This instance function is a thin wrapper around the Authy client function that sends a candidate password to be verified. We call Authy's built-in verify(link takes you to an external page) function, and then immediately call a passed callback function with the result.

Verify the user's Authy token

verify-the-users-authy-token page anchor

models/User.js


_128
const mongoose = require('mongoose');
_128
const bcrypt = require('bcrypt');
_128
const config = require('../config');
_128
_128
// Create authenticated Authy and Twilio API clients
_128
const authy = require('authy')(config.authyKey);
_128
const twilioClient = require('twilio')(config.accountSid, config.authToken);
_128
_128
// Used to generate password hash
_128
const SALT_WORK_FACTOR = 10;
_128
_128
// Define user model schema
_128
const UserSchema = new mongoose.Schema({
_128
fullName: {
_128
type: String,
_128
required: true,
_128
},
_128
countryCode: {
_128
type: String,
_128
required: true,
_128
},
_128
phone: {
_128
type: String,
_128
required: true,
_128
},
_128
verified: {
_128
type: Boolean,
_128
default: false,
_128
},
_128
authyId: String,
_128
email: {
_128
type: String,
_128
required: true,
_128
unique: true,
_128
},
_128
password: {
_128
type: String,
_128
required: true,
_128
},
_128
});
_128
_128
// Middleware executed before save - hash the user's password
_128
UserSchema.pre('save', function(next) {
_128
const self = this;
_128
_128
// only hash the password if it has been modified (or is new)
_128
if (!self.isModified('password')) return next();
_128
_128
// generate a salt
_128
bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) {
_128
if (err) return next(err);
_128
_128
// hash the password using our new salt
_128
bcrypt.hash(self.password, salt, function(err, hash) {
_128
if (err) return next(err);
_128
_128
// override the cleartext password with the hashed one
_128
self.password = hash;
_128
next();
_128
});
_128
});
_128
});
_128
_128
// Test candidate password
_128
UserSchema.methods.comparePassword = function(candidatePassword, cb) {
_128
const self = this;
_128
bcrypt.compare(candidatePassword, self.password, function(err, isMatch) {
_128
if (err) return cb(err);
_128
cb(null, isMatch);
_128
});
_128
};
_128
_128
// Send a verification token to this user
_128
UserSchema.methods.sendAuthyToken = function(cb) {
_128
var self = this;
_128
_128
if (!self.authyId) {
_128
// Register this user if it's a new user
_128
authy.register_user(self.email, self.phone, self.countryCode,
_128
function(err, response) {
_128
if (err || !response.user) return cb.call(self, err);
_128
self.authyId = response.user.id;
_128
self.save(function(err, doc) {
_128
if (err || !doc) return cb.call(self, err);
_128
self = doc;
_128
sendToken();
_128
});
_128
});
_128
} else {
_128
// Otherwise send token to a known user
_128
sendToken();
_128
}
_128
_128
// With a valid Authy ID, send the 2FA token for this user
_128
function sendToken() {
_128
authy.request_sms(self.authyId, true, function(err, response) {
_128
cb.call(self, err);
_128
});
_128
}
_128
};
_128
_128
// Test a 2FA token
_128
UserSchema.methods.verifyAuthyToken = function(otp, cb) {
_128
const self = this;
_128
authy.verify(self.authyId, otp, function(err, response) {
_128
cb.call(self, err, response);
_128
});
_128
};
_128
_128
// Send a text message via twilio to this user
_128
UserSchema.methods.sendMessage =
_128
function(message, successCallback, errorCallback) {
_128
const self = this;
_128
const toNumber = `+${self.countryCode}${self.phone}`;
_128
_128
twilioClient.messages.create({
_128
to: toNumber,
_128
from: config.twilioNumber,
_128
body: message,
_128
}).then(function() {
_128
successCallback();
_128
}).catch(function(err) {
_128
errorCallback(err);
_128
});
_128
};
_128
_128
// Export user model
_128
module.exports = mongoose.model('User', UserSchema);

This is a great start, but what if your code never reaches the end user's handset? Authy can help us to re-send a missing code.


This controller function loads the User model associated with the request and then uses the same instance function we defined earlier to resend the code.

controllers/users.js


_164
const User = require('../models/User');
_164
_164
// Display a form that allows users to sign up for a new account
_164
exports.showCreate = function(request, response) {
_164
response.render('users/create', {
_164
title: 'Create User Account',
_164
// include any errors (success messages not possible for view)
_164
errors: request.flash('errors'),
_164
});
_164
};
_164
_164
// create a new user based on the form submission
_164
exports.create = function(request, response) {
_164
const params = request.body;
_164
_164
// Create a new user based on form parameters
_164
const user = new User({
_164
fullName: params.fullName,
_164
email: params.email,
_164
phone: params.phone,
_164
countryCode: params.countryCode,
_164
password: params.password,
_164
});
_164
_164
user.save(function(err, doc) {
_164
if (err) {
_164
// To improve on this example, you should include a better
_164
// error message, especially around form field validation. But
_164
// for now, just indicate that the save operation failed
_164
request.flash('errors', 'There was a problem creating your'
_164
+ ' account - note that all fields are required. Please'
_164
+ ' double-check your input and try again.');
_164
_164
response.redirect('/users/new');
_164
} else {
_164
// If the user is created successfully, send them an account
_164
// verification token
_164
user.sendAuthyToken(function(err) {
_164
if (err) {
_164
request.flash('errors', 'There was a problem sending '
_164
+ 'your token - sorry :(');
_164
}
_164
_164
// Send to token verification page
_164
response.redirect('/users/'+doc._id+'/verify');
_164
});
_164
}
_164
});
_164
};
_164
_164
// Display a form that allows users to enter a verification token
_164
exports.showVerify = function(request, response) {
_164
response.render('users/verify', {
_164
title: 'Verify Phone Number',
_164
// include any errors
_164
errors: request.flash('errors'),
_164
// success messsages
_164
successes: request.flash('successes'),
_164
// Include database ID to include in form POST action
_164
id: request.params.id,
_164
});
_164
};
_164
_164
// Resend a code if it was not received
_164
exports.resend = function(request, response) {
_164
// Load user model
_164
User.findById(request.params.id, function(err, user) {
_164
if (err || !user) {
_164
return die('User not found for this ID.');
_164
}
_164
_164
// If we find the user, let's send them a new code
_164
user.sendAuthyToken(postSend);
_164
});
_164
_164
// Handle send code response
_164
function postSend(err) {
_164
if (err) {
_164
return die('There was a problem sending you the code - please '
_164
+ 'retry.');
_164
}
_164
_164
request.flash('successes', 'Code re-sent!');
_164
response.redirect('/users/'+request.params.id+'/verify');
_164
}
_164
_164
// respond with an error
_164
function die(message) {
_164
request.flash('errors', message);
_164
response.redirect('/users/'+request.params.id+'/verify');
_164
}
_164
};
_164
_164
// Handle submission of verification token
_164
exports.verify = function(request, response) {
_164
let user = {};
_164
_164
// Load user model
_164
User.findById(request.params.id, function(err, doc) {
_164
if (err || !doc) {
_164
return die('User not found for this ID.');
_164
}
_164
_164
// If we find the user, let's validate the token they entered
_164
user = doc;
_164
user.verifyAuthyToken(request.body.code, postVerify);
_164
});
_164
_164
// Handle verification response
_164
function postVerify(err) {
_164
if (err) {
_164
return die('The token you entered was invalid - please retry.');
_164
}
_164
_164
// If the token was valid, flip the bit to validate the user account
_164
user.verified = true;
_164
user.save(postSave);
_164
}
_164
_164
// after we save the user, handle sending a confirmation
_164
function postSave(err) {
_164
if (err) {
_164
return die('There was a problem validating your account '
_164
+ '- please enter your token again.');
_164
}
_164
_164
// Send confirmation text message
_164
const message = 'You did it! Signup complete :)';
_164
user.sendMessage(message, function() {
_164
// show success page
_164
request.flash('successes', message);
_164
response.redirect(`/users/${user._id}`);
_164
}, function(err) {
_164
request.flash('errors', 'You are signed up, but '
_164
+ 'we could not send you a message. Our bad :(');
_164
});
_164
}
_164
_164
// respond with an error
_164
function die(message) {
_164
request.flash('errors', message);
_164
response.redirect('/users/'+request.params.id+'/verify');
_164
}
_164
};
_164
_164
// Show details about the user
_164
exports.showUser = function(request, response, next) {
_164
// Load user model
_164
User.findById(request.params.id, function(err, user) {
_164
if (err || !user) {
_164
// 404
_164
return next();
_164
}
_164
_164
response.render('users/show', {
_164
title: 'Hi there ' + user.fullName + '!',
_164
user: user,
_164
// any errors
_164
errors: request.flash('errors'),
_164
// any success messages
_164
successes: request.flash('successes'),
_164
});
_164
});
_164
};

To wrap things up, let's let the user know that their account has been verified via a success page and an SMS to their device.


This controller function renders a Jade template that contains the user's full name, and indicates whether or not they are verified by checking the user's verified property.

Show details about a user

show-details-about-a-user page anchor

controllers/users.js


_164
const User = require('../models/User');
_164
_164
// Display a form that allows users to sign up for a new account
_164
exports.showCreate = function(request, response) {
_164
response.render('users/create', {
_164
title: 'Create User Account',
_164
// include any errors (success messages not possible for view)
_164
errors: request.flash('errors'),
_164
});
_164
};
_164
_164
// create a new user based on the form submission
_164
exports.create = function(request, response) {
_164
const params = request.body;
_164
_164
// Create a new user based on form parameters
_164
const user = new User({
_164
fullName: params.fullName,
_164
email: params.email,
_164
phone: params.phone,
_164
countryCode: params.countryCode,
_164
password: params.password,
_164
});
_164
_164
user.save(function(err, doc) {
_164
if (err) {
_164
// To improve on this example, you should include a better
_164
// error message, especially around form field validation. But
_164
// for now, just indicate that the save operation failed
_164
request.flash('errors', 'There was a problem creating your'
_164
+ ' account - note that all fields are required. Please'
_164
+ ' double-check your input and try again.');
_164
_164
response.redirect('/users/new');
_164
} else {
_164
// If the user is created successfully, send them an account
_164
// verification token
_164
user.sendAuthyToken(function(err) {
_164
if (err) {
_164
request.flash('errors', 'There was a problem sending '
_164
+ 'your token - sorry :(');
_164
}
_164
_164
// Send to token verification page
_164
response.redirect('/users/'+doc._id+'/verify');
_164
});
_164
}
_164
});
_164
};
_164
_164
// Display a form that allows users to enter a verification token
_164
exports.showVerify = function(request, response) {
_164
response.render('users/verify', {
_164
title: 'Verify Phone Number',
_164
// include any errors
_164
errors: request.flash('errors'),
_164
// success messsages
_164
successes: request.flash('successes'),
_164
// Include database ID to include in form POST action
_164
id: request.params.id,
_164
});
_164
};
_164
_164
// Resend a code if it was not received
_164
exports.resend = function(request, response) {
_164
// Load user model
_164
User.findById(request.params.id, function(err, user) {
_164
if (err || !user) {
_164
return die('User not found for this ID.');
_164
}
_164
_164
// If we find the user, let's send them a new code
_164
user.sendAuthyToken(postSend);
_164
});
_164
_164
// Handle send code response
_164
function postSend(err) {
_164
if (err) {
_164
return die('There was a problem sending you the code - please '
_164
+ 'retry.');
_164
}
_164
_164
request.flash('successes', 'Code re-sent!');
_164
response.redirect('/users/'+request.params.id+'/verify');
_164
}
_164
_164
// respond with an error
_164
function die(message) {
_164
request.flash('errors', message);
_164
response.redirect('/users/'+request.params.id+'/verify');
_164
}
_164
};
_164
_164
// Handle submission of verification token
_164
exports.verify = function(request, response) {
_164
let user = {};
_164
_164
// Load user model
_164
User.findById(request.params.id, function(err, doc) {
_164
if (err || !doc) {
_164
return die('User not found for this ID.');
_164
}
_164
_164
// If we find the user, let's validate the token they entered
_164
user = doc;
_164
user.verifyAuthyToken(request.body.code, postVerify);
_164
});
_164
_164
// Handle verification response
_164
function postVerify(err) {
_164
if (err) {
_164
return die('The token you entered was invalid - please retry.');
_164
}
_164
_164
// If the token was valid, flip the bit to validate the user account
_164
user.verified = true;
_164
user.save(postSave);
_164
}
_164
_164
// after we save the user, handle sending a confirmation
_164
function postSave(err) {
_164
if (err) {
_164
return die('There was a problem validating your account '
_164
+ '- please enter your token again.');
_164
}
_164
_164
// Send confirmation text message
_164
const message = 'You did it! Signup complete :)';
_164
user.sendMessage(message, function() {
_164
// show success page
_164
request.flash('successes', message);
_164
response.redirect(`/users/${user._id}`);
_164
}, function(err) {
_164
request.flash('errors', 'You are signed up, but '
_164
+ 'we could not send you a message. Our bad :(');
_164
});
_164
}
_164
_164
// respond with an error
_164
function die(message) {
_164
request.flash('errors', message);
_164
response.redirect('/users/'+request.params.id+'/verify');
_164
}
_164
};
_164
_164
// Show details about the user
_164
exports.showUser = function(request, response, next) {
_164
// Load user model
_164
User.findById(request.params.id, function(err, user) {
_164
if (err || !user) {
_164
// 404
_164
return next();
_164
}
_164
_164
response.render('users/show', {
_164
title: 'Hi there ' + user.fullName + '!',
_164
user: user,
_164
// any errors
_164
errors: request.flash('errors'),
_164
// any success messages
_164
successes: request.flash('successes'),
_164
});
_164
});
_164
};

This should suffice for confirmation in the browser that the user has been verified. Let's see how we might send a confirmation via text message.


Here, we add another instance function to the model that will send a text message to the user's configured phone number. Rather than just being a one-time password, this can be anything we wish.

Send a text message via Twilio to a user

send-a-text-message-via-twilio-to-a-user page anchor

models/User.js


_128
const mongoose = require('mongoose');
_128
const bcrypt = require('bcrypt');
_128
const config = require('../config');
_128
_128
// Create authenticated Authy and Twilio API clients
_128
const authy = require('authy')(config.authyKey);
_128
const twilioClient = require('twilio')(config.accountSid, config.authToken);
_128
_128
// Used to generate password hash
_128
const SALT_WORK_FACTOR = 10;
_128
_128
// Define user model schema
_128
const UserSchema = new mongoose.Schema({
_128
fullName: {
_128
type: String,
_128
required: true,
_128
},
_128
countryCode: {
_128
type: String,
_128
required: true,
_128
},
_128
phone: {
_128
type: String,
_128
required: true,
_128
},
_128
verified: {
_128
type: Boolean,
_128
default: false,
_128
},
_128
authyId: String,
_128
email: {
_128
type: String,
_128
required: true,
_128
unique: true,
_128
},
_128
password: {
_128
type: String,
_128
required: true,
_128
},
_128
});
_128
_128
// Middleware executed before save - hash the user's password
_128
UserSchema.pre('save', function(next) {
_128
const self = this;
_128
_128
// only hash the password if it has been modified (or is new)
_128
if (!self.isModified('password')) return next();
_128
_128
// generate a salt
_128
bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) {
_128
if (err) return next(err);
_128
_128
// hash the password using our new salt
_128
bcrypt.hash(self.password, salt, function(err, hash) {
_128
if (err) return next(err);
_128
_128
// override the cleartext password with the hashed one
_128
self.password = hash;
_128
next();
_128
});
_128
});
_128
});
_128
_128
// Test candidate password
_128
UserSchema.methods.comparePassword = function(candidatePassword, cb) {
_128
const self = this;
_128
bcrypt.compare(candidatePassword, self.password, function(err, isMatch) {
_128
if (err) return cb(err);
_128
cb(null, isMatch);
_128
});
_128
};
_128
_128
// Send a verification token to this user
_128
UserSchema.methods.sendAuthyToken = function(cb) {
_128
var self = this;
_128
_128
if (!self.authyId) {
_128
// Register this user if it's a new user
_128
authy.register_user(self.email, self.phone, self.countryCode,
_128
function(err, response) {
_128
if (err || !response.user) return cb.call(self, err);
_128
self.authyId = response.user.id;
_128
self.save(function(err, doc) {
_128
if (err || !doc) return cb.call(self, err);
_128
self = doc;
_128
sendToken();
_128
});
_128
});
_128
} else {
_128
// Otherwise send token to a known user
_128
sendToken();
_128
}
_128
_128
// With a valid Authy ID, send the 2FA token for this user
_128
function sendToken() {
_128
authy.request_sms(self.authyId, true, function(err, response) {
_128
cb.call(self, err);
_128
});
_128
}
_128
};
_128
_128
// Test a 2FA token
_128
UserSchema.methods.verifyAuthyToken = function(otp, cb) {
_128
const self = this;
_128
authy.verify(self.authyId, otp, function(err, response) {
_128
cb.call(self, err, response);
_128
});
_128
};
_128
_128
// Send a text message via twilio to this user
_128
UserSchema.methods.sendMessage =
_128
function(message, successCallback, errorCallback) {
_128
const self = this;
_128
const toNumber = `+${self.countryCode}${self.phone}`;
_128
_128
twilioClient.messages.create({
_128
to: toNumber,
_128
from: config.twilioNumber,
_128
body: message,
_128
}).then(function() {
_128
successCallback();
_128
}).catch(function(err) {
_128
errorCallback(err);
_128
});
_128
};
_128
_128
// Export user model
_128
module.exports = mongoose.model('User', UserSchema);

Congratulations! You now have the power to register and verify users with Authy and Twilio SMS. Where can we take it from here?


If you're a Node developer working with Twilio, you might want to check out these other tutorials:

Click-To-Call

Put a button on your web page that connects visitors to live support or salespeople via telephone.

Automated Survey

Instantly collect structured data from your users with a survey conducted over a call or SMS text messages.

Did this help?

did-this-help page anchor

Thanks for checking this tutorial out! If you have any feedback to share with us, we'd love to hear it. Reach out to us on Twitter(link takes you to an external page) and let us know what you build!


Rate this page: