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.
Ready to implement user account verification in your application? Here's how it works at a high level:
To get this done, you'll be working with the following Twilio-powered APIs:
Authy REST API
Twilio REST API
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 for persistence, but in our code we will primarily interface with Mongoose, 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.
models/User.js
_128const mongoose = require('mongoose');_128const bcrypt = require('bcrypt');_128const config = require('../config');_128_128// Create authenticated Authy and Twilio API clients_128const authy = require('authy')(config.authyKey);_128const twilioClient = require('twilio')(config.accountSid, config.authToken);_128_128// Used to generate password hash_128const SALT_WORK_FACTOR = 10;_128_128// Define user model schema_128const 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_128UserSchema.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_128UserSchema.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_128UserSchema.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_128UserSchema.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_128UserSchema.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_128module.exports = mongoose.model('User', UserSchema);
Now that you've created your user model, let's check out the form template for creating a user.
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.
views/users/create.jade
_41extends ../layout_41_41block styles_41 link(rel='stylesheet', media='screen'_41 href='//www.authy.com/form.authy.min.css')_41_41block 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_41block 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). Once you create an Authy application, the production key is found on the dashboard:
config.js
_45const dotenvSafe = require('dotenv-safe');_45_45const nodeEnv = process.env.NODE_ENV;_45if(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}_45dotenvSafe.load();_45_45const cfg = {};_45_45// HTTP Port to run our web application_45cfg.port = process.env.PORT || 3000;_45_45// A random string that will help generate secure one-time passwords and_45// HTTP sessions_45cfg.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._45cfg.accountSid = process.env.TWILIO_ACCOUNT_SID;_45cfg.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"_45cfg.twilioNumber = process.env.TWILIO_NUMBER;_45_45// Your Authy production key - this can be found on the dashboard for your_45// Authy application_45cfg.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_45cfg.mongoUrl = process.env.MONGODB_URI || process.env.MONGO_URL;_45_45// Export configuration object_45module.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.
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.
models/User.js
_128const mongoose = require('mongoose');_128const bcrypt = require('bcrypt');_128const config = require('../config');_128_128// Create authenticated Authy and Twilio API clients_128const authy = require('authy')(config.authyKey);_128const twilioClient = require('twilio')(config.accountSid, config.authToken);_128_128// Used to generate password hash_128const SALT_WORK_FACTOR = 10;_128_128// Define user model schema_128const 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_128UserSchema.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_128UserSchema.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_128UserSchema.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_128UserSchema.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_128UserSchema.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_128module.exports = mongoose.model('User', UserSchema);
After the user receives the verification code, they will pass it to the application using this form.
Let's check out the controller that handles the form.
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:
User
model for the current verification request.
Take a look at the User
model to see the instance method that handles verifying the code with Authy.
controllers/users.js
_164const User = require('../models/User');_164_164// Display a form that allows users to sign up for a new account_164exports.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_164exports.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_164exports.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_164exports.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_164exports.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_164exports.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.
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 function, and then immediately call a passed callback function with the result.
models/User.js
_128const mongoose = require('mongoose');_128const bcrypt = require('bcrypt');_128const config = require('../config');_128_128// Create authenticated Authy and Twilio API clients_128const authy = require('authy')(config.authyKey);_128const twilioClient = require('twilio')(config.accountSid, config.authToken);_128_128// Used to generate password hash_128const SALT_WORK_FACTOR = 10;_128_128// Define user model schema_128const 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_128UserSchema.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_128UserSchema.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_128UserSchema.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_128UserSchema.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_128UserSchema.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_128module.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
_164const User = require('../models/User');_164_164// Display a form that allows users to sign up for a new account_164exports.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_164exports.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_164exports.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_164exports.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_164exports.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_164exports.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.
controllers/users.js
_164const User = require('../models/User');_164_164// Display a form that allows users to sign up for a new account_164exports.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_164exports.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_164exports.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_164exports.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_164exports.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_164exports.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.
models/User.js
_128const mongoose = require('mongoose');_128const bcrypt = require('bcrypt');_128const config = require('../config');_128_128// Create authenticated Authy and Twilio API clients_128const authy = require('authy')(config.authyKey);_128const twilioClient = require('twilio')(config.accountSid, config.authToken);_128_128// Used to generate password hash_128const SALT_WORK_FACTOR = 10;_128_128// Define user model schema_128const 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_128UserSchema.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_128UserSchema.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_128UserSchema.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_128UserSchema.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_128UserSchema.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_128module.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:
Put a button on your web page that connects visitors to live support or salespeople via telephone.
Instantly collect structured data from your users with a survey conducted over a call or SMS text messages.
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 and let us know what you build!