Skip to contentSkip to navigationSkip to topbar
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

1
const mongoose = require('mongoose');
2
const bcrypt = require('bcrypt');
3
const config = require('../config');
4
5
// Create authenticated Authy and Twilio API clients
6
const authy = require('authy')(config.authyKey);
7
const twilioClient = require('twilio')(config.accountSid, config.authToken);
8
9
// Used to generate password hash
10
const SALT_WORK_FACTOR = 10;
11
12
// Define user model schema
13
const UserSchema = new mongoose.Schema({
14
fullName: {
15
type: String,
16
required: true,
17
},
18
countryCode: {
19
type: String,
20
required: true,
21
},
22
phone: {
23
type: String,
24
required: true,
25
},
26
verified: {
27
type: Boolean,
28
default: false,
29
},
30
authyId: String,
31
email: {
32
type: String,
33
required: true,
34
unique: true,
35
},
36
password: {
37
type: String,
38
required: true,
39
},
40
});
41
42
// Middleware executed before save - hash the user's password
43
UserSchema.pre('save', function(next) {
44
const self = this;
45
46
// only hash the password if it has been modified (or is new)
47
if (!self.isModified('password')) return next();
48
49
// generate a salt
50
bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) {
51
if (err) return next(err);
52
53
// hash the password using our new salt
54
bcrypt.hash(self.password, salt, function(err, hash) {
55
if (err) return next(err);
56
57
// override the cleartext password with the hashed one
58
self.password = hash;
59
next();
60
});
61
});
62
});
63
64
// Test candidate password
65
UserSchema.methods.comparePassword = function(candidatePassword, cb) {
66
const self = this;
67
bcrypt.compare(candidatePassword, self.password, function(err, isMatch) {
68
if (err) return cb(err);
69
cb(null, isMatch);
70
});
71
};
72
73
// Send a verification token to this user
74
UserSchema.methods.sendAuthyToken = function(cb) {
75
var self = this;
76
77
if (!self.authyId) {
78
// Register this user if it's a new user
79
authy.register_user(self.email, self.phone, self.countryCode,
80
function(err, response) {
81
if (err || !response.user) return cb.call(self, err);
82
self.authyId = response.user.id;
83
self.save(function(err, doc) {
84
if (err || !doc) return cb.call(self, err);
85
self = doc;
86
sendToken();
87
});
88
});
89
} else {
90
// Otherwise send token to a known user
91
sendToken();
92
}
93
94
// With a valid Authy ID, send the 2FA token for this user
95
function sendToken() {
96
authy.request_sms(self.authyId, true, function(err, response) {
97
cb.call(self, err);
98
});
99
}
100
};
101
102
// Test a 2FA token
103
UserSchema.methods.verifyAuthyToken = function(otp, cb) {
104
const self = this;
105
authy.verify(self.authyId, otp, function(err, response) {
106
cb.call(self, err, response);
107
});
108
};
109
110
// Send a text message via twilio to this user
111
UserSchema.methods.sendMessage =
112
function(message, successCallback, errorCallback) {
113
const self = this;
114
const toNumber = `+${self.countryCode}${self.phone}`;
115
116
twilioClient.messages.create({
117
to: toNumber,
118
from: config.twilioNumber,
119
body: message,
120
}).then(function() {
121
successCallback();
122
}).catch(function(err) {
123
errorCallback(err);
124
});
125
};
126
127
// Export user model
128
module.exports = mongoose.model('User', UserSchema);
129

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

1
extends ../layout
2
3
block styles
4
link(rel='stylesheet', media='screen'
5
href='//www.authy.com/form.authy.min.css')
6
7
block content
8
h1 We're Going To Be *BEST* Friends
9
p.
10
Thank you for your interest in signing up! Can you tell us a bit about
11
yourself?
12
13
form(action='/users', method='POST')
14
.form-group
15
label(for='fullName') Your Full Name:
16
input.form-control(type='text', name='fullName',
17
placeholder='Peggy Carter')
18
19
.form-group
20
label(for='email') Your email Address:
21
input.form-control(type='email', name='email',
22
placeholder='pcarter@ssr.gov')
23
24
.form-group
25
label(for='password') Your Password:
26
input.form-control(type='password', name='password')
27
28
.form-group
29
label(for='countryCode') Country Code:
30
select(id='authy-countries', name='countryCode')
31
32
.form-group
33
label(for='phone') Mobile Phone:
34
input.form-control(type='text', name='phone', placeholder='651-867-5309')
35
36
button.btn.btn-primary(type='submit') Create Account
37
38
block scripts
39
// Include Authy form helpers and additional helper script
40
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

1
const dotenvSafe = require('dotenv-safe');
2
3
const nodeEnv = process.env.NODE_ENV;
4
if(nodeEnv && nodeEnv === 'production') {
5
// If it's running in Heroku, we set MONGO_URL to an arbitrary value so that
6
// dotenv-safe doesn't throw an error. MONGO_URL is not read in Heroku as
7
// MONGODB_URI will be set
8
process.env.MONGO_URL = 'placeholder';
9
}
10
dotenvSafe.load();
11
12
const cfg = {};
13
14
// HTTP Port to run our web application
15
cfg.port = process.env.PORT || 3000;
16
17
// A random string that will help generate secure one-time passwords and
18
// HTTP sessions
19
cfg.secret = process.env.APP_SECRET || 'keyboard cat';
20
21
// Your Twilio account SID and auth token, both found at:
22
// https://www.twilio.com/user/account
23
//
24
// A good practice is to store these string values as system environment
25
// variables, and load them from there as we are doing below. Alternately,
26
// you could hard code these values here as strings.
27
cfg.accountSid = process.env.TWILIO_ACCOUNT_SID;
28
cfg.authToken = process.env.TWILIO_AUTH_TOKEN;
29
30
// A Twilio number you control - choose one from:
31
// https://www.twilio.com/user/account/phone-numbers/incoming
32
// Specify in E.164 format, e.g. "+16519998877"
33
cfg.twilioNumber = process.env.TWILIO_NUMBER;
34
35
// Your Authy production key - this can be found on the dashboard for your
36
// Authy application
37
cfg.authyKey = process.env.AUTHY_API_KEY;
38
39
// MongoDB connection string - MONGO_URL is for local dev,
40
// MONGODB_URI is for the MongoLab add-on for Heroku deployment
41
// when using docker-compose
42
cfg.mongoUrl = process.env.MONGODB_URI || process.env.MONGO_URL;
43
44
// Export configuration object
45
module.exports = cfg;
46

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

1
const mongoose = require('mongoose');
2
const bcrypt = require('bcrypt');
3
const config = require('../config');
4
5
// Create authenticated Authy and Twilio API clients
6
const authy = require('authy')(config.authyKey);
7
const twilioClient = require('twilio')(config.accountSid, config.authToken);
8
9
// Used to generate password hash
10
const SALT_WORK_FACTOR = 10;
11
12
// Define user model schema
13
const UserSchema = new mongoose.Schema({
14
fullName: {
15
type: String,
16
required: true,
17
},
18
countryCode: {
19
type: String,
20
required: true,
21
},
22
phone: {
23
type: String,
24
required: true,
25
},
26
verified: {
27
type: Boolean,
28
default: false,
29
},
30
authyId: String,
31
email: {
32
type: String,
33
required: true,
34
unique: true,
35
},
36
password: {
37
type: String,
38
required: true,
39
},
40
});
41
42
// Middleware executed before save - hash the user's password
43
UserSchema.pre('save', function(next) {
44
const self = this;
45
46
// only hash the password if it has been modified (or is new)
47
if (!self.isModified('password')) return next();
48
49
// generate a salt
50
bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) {
51
if (err) return next(err);
52
53
// hash the password using our new salt
54
bcrypt.hash(self.password, salt, function(err, hash) {
55
if (err) return next(err);
56
57
// override the cleartext password with the hashed one
58
self.password = hash;
59
next();
60
});
61
});
62
});
63
64
// Test candidate password
65
UserSchema.methods.comparePassword = function(candidatePassword, cb) {
66
const self = this;
67
bcrypt.compare(candidatePassword, self.password, function(err, isMatch) {
68
if (err) return cb(err);
69
cb(null, isMatch);
70
});
71
};
72
73
// Send a verification token to this user
74
UserSchema.methods.sendAuthyToken = function(cb) {
75
var self = this;
76
77
if (!self.authyId) {
78
// Register this user if it's a new user
79
authy.register_user(self.email, self.phone, self.countryCode,
80
function(err, response) {
81
if (err || !response.user) return cb.call(self, err);
82
self.authyId = response.user.id;
83
self.save(function(err, doc) {
84
if (err || !doc) return cb.call(self, err);
85
self = doc;
86
sendToken();
87
});
88
});
89
} else {
90
// Otherwise send token to a known user
91
sendToken();
92
}
93
94
// With a valid Authy ID, send the 2FA token for this user
95
function sendToken() {
96
authy.request_sms(self.authyId, true, function(err, response) {
97
cb.call(self, err);
98
});
99
}
100
};
101
102
// Test a 2FA token
103
UserSchema.methods.verifyAuthyToken = function(otp, cb) {
104
const self = this;
105
authy.verify(self.authyId, otp, function(err, response) {
106
cb.call(self, err, response);
107
});
108
};
109
110
// Send a text message via twilio to this user
111
UserSchema.methods.sendMessage =
112
function(message, successCallback, errorCallback) {
113
const self = this;
114
const toNumber = `+${self.countryCode}${self.phone}`;
115
116
twilioClient.messages.create({
117
to: toNumber,
118
from: config.twilioNumber,
119
body: message,
120
}).then(function() {
121
successCallback();
122
}).catch(function(err) {
123
errorCallback(err);
124
});
125
};
126
127
// Export user model
128
module.exports = mongoose.model('User', UserSchema);
129

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

1
const User = require('../models/User');
2
3
// Display a form that allows users to sign up for a new account
4
exports.showCreate = function(request, response) {
5
response.render('users/create', {
6
title: 'Create User Account',
7
// include any errors (success messages not possible for view)
8
errors: request.flash('errors'),
9
});
10
};
11
12
// create a new user based on the form submission
13
exports.create = function(request, response) {
14
const params = request.body;
15
16
// Create a new user based on form parameters
17
const user = new User({
18
fullName: params.fullName,
19
email: params.email,
20
phone: params.phone,
21
countryCode: params.countryCode,
22
password: params.password,
23
});
24
25
user.save(function(err, doc) {
26
if (err) {
27
// To improve on this example, you should include a better
28
// error message, especially around form field validation. But
29
// for now, just indicate that the save operation failed
30
request.flash('errors', 'There was a problem creating your'
31
+ ' account - note that all fields are required. Please'
32
+ ' double-check your input and try again.');
33
34
response.redirect('/users/new');
35
} else {
36
// If the user is created successfully, send them an account
37
// verification token
38
user.sendAuthyToken(function(err) {
39
if (err) {
40
request.flash('errors', 'There was a problem sending '
41
+ 'your token - sorry :(');
42
}
43
44
// Send to token verification page
45
response.redirect('/users/'+doc._id+'/verify');
46
});
47
}
48
});
49
};
50
51
// Display a form that allows users to enter a verification token
52
exports.showVerify = function(request, response) {
53
response.render('users/verify', {
54
title: 'Verify Phone Number',
55
// include any errors
56
errors: request.flash('errors'),
57
// success messsages
58
successes: request.flash('successes'),
59
// Include database ID to include in form POST action
60
id: request.params.id,
61
});
62
};
63
64
// Resend a code if it was not received
65
exports.resend = function(request, response) {
66
// Load user model
67
User.findById(request.params.id, function(err, user) {
68
if (err || !user) {
69
return die('User not found for this ID.');
70
}
71
72
// If we find the user, let's send them a new code
73
user.sendAuthyToken(postSend);
74
});
75
76
// Handle send code response
77
function postSend(err) {
78
if (err) {
79
return die('There was a problem sending you the code - please '
80
+ 'retry.');
81
}
82
83
request.flash('successes', 'Code re-sent!');
84
response.redirect('/users/'+request.params.id+'/verify');
85
}
86
87
// respond with an error
88
function die(message) {
89
request.flash('errors', message);
90
response.redirect('/users/'+request.params.id+'/verify');
91
}
92
};
93
94
// Handle submission of verification token
95
exports.verify = function(request, response) {
96
let user = {};
97
98
// Load user model
99
User.findById(request.params.id, function(err, doc) {
100
if (err || !doc) {
101
return die('User not found for this ID.');
102
}
103
104
// If we find the user, let's validate the token they entered
105
user = doc;
106
user.verifyAuthyToken(request.body.code, postVerify);
107
});
108
109
// Handle verification response
110
function postVerify(err) {
111
if (err) {
112
return die('The token you entered was invalid - please retry.');
113
}
114
115
// If the token was valid, flip the bit to validate the user account
116
user.verified = true;
117
user.save(postSave);
118
}
119
120
// after we save the user, handle sending a confirmation
121
function postSave(err) {
122
if (err) {
123
return die('There was a problem validating your account '
124
+ '- please enter your token again.');
125
}
126
127
// Send confirmation text message
128
const message = 'You did it! Signup complete :)';
129
user.sendMessage(message, function() {
130
// show success page
131
request.flash('successes', message);
132
response.redirect(`/users/${user._id}`);
133
}, function(err) {
134
request.flash('errors', 'You are signed up, but '
135
+ 'we could not send you a message. Our bad :(');
136
});
137
}
138
139
// respond with an error
140
function die(message) {
141
request.flash('errors', message);
142
response.redirect('/users/'+request.params.id+'/verify');
143
}
144
};
145
146
// Show details about the user
147
exports.showUser = function(request, response, next) {
148
// Load user model
149
User.findById(request.params.id, function(err, user) {
150
if (err || !user) {
151
// 404
152
return next();
153
}
154
155
response.render('users/show', {
156
title: 'Hi there ' + user.fullName + '!',
157
user: user,
158
// any errors
159
errors: request.flash('errors'),
160
// any success messages
161
successes: request.flash('successes'),
162
});
163
});
164
};
165

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

1
const mongoose = require('mongoose');
2
const bcrypt = require('bcrypt');
3
const config = require('../config');
4
5
// Create authenticated Authy and Twilio API clients
6
const authy = require('authy')(config.authyKey);
7
const twilioClient = require('twilio')(config.accountSid, config.authToken);
8
9
// Used to generate password hash
10
const SALT_WORK_FACTOR = 10;
11
12
// Define user model schema
13
const UserSchema = new mongoose.Schema({
14
fullName: {
15
type: String,
16
required: true,
17
},
18
countryCode: {
19
type: String,
20
required: true,
21
},
22
phone: {
23
type: String,
24
required: true,
25
},
26
verified: {
27
type: Boolean,
28
default: false,
29
},
30
authyId: String,
31
email: {
32
type: String,
33
required: true,
34
unique: true,
35
},
36
password: {
37
type: String,
38
required: true,
39
},
40
});
41
42
// Middleware executed before save - hash the user's password
43
UserSchema.pre('save', function(next) {
44
const self = this;
45
46
// only hash the password if it has been modified (or is new)
47
if (!self.isModified('password')) return next();
48
49
// generate a salt
50
bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) {
51
if (err) return next(err);
52
53
// hash the password using our new salt
54
bcrypt.hash(self.password, salt, function(err, hash) {
55
if (err) return next(err);
56
57
// override the cleartext password with the hashed one
58
self.password = hash;
59
next();
60
});
61
});
62
});
63
64
// Test candidate password
65
UserSchema.methods.comparePassword = function(candidatePassword, cb) {
66
const self = this;
67
bcrypt.compare(candidatePassword, self.password, function(err, isMatch) {
68
if (err) return cb(err);
69
cb(null, isMatch);
70
});
71
};
72
73
// Send a verification token to this user
74
UserSchema.methods.sendAuthyToken = function(cb) {
75
var self = this;
76
77
if (!self.authyId) {
78
// Register this user if it's a new user
79
authy.register_user(self.email, self.phone, self.countryCode,
80
function(err, response) {
81
if (err || !response.user) return cb.call(self, err);
82
self.authyId = response.user.id;
83
self.save(function(err, doc) {
84
if (err || !doc) return cb.call(self, err);
85
self = doc;
86
sendToken();
87
});
88
});
89
} else {
90
// Otherwise send token to a known user
91
sendToken();
92
}
93
94
// With a valid Authy ID, send the 2FA token for this user
95
function sendToken() {
96
authy.request_sms(self.authyId, true, function(err, response) {
97
cb.call(self, err);
98
});
99
}
100
};
101
102
// Test a 2FA token
103
UserSchema.methods.verifyAuthyToken = function(otp, cb) {
104
const self = this;
105
authy.verify(self.authyId, otp, function(err, response) {
106
cb.call(self, err, response);
107
});
108
};
109
110
// Send a text message via twilio to this user
111
UserSchema.methods.sendMessage =
112
function(message, successCallback, errorCallback) {
113
const self = this;
114
const toNumber = `+${self.countryCode}${self.phone}`;
115
116
twilioClient.messages.create({
117
to: toNumber,
118
from: config.twilioNumber,
119
body: message,
120
}).then(function() {
121
successCallback();
122
}).catch(function(err) {
123
errorCallback(err);
124
});
125
};
126
127
// Export user model
128
module.exports = mongoose.model('User', UserSchema);
129

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

1
const User = require('../models/User');
2
3
// Display a form that allows users to sign up for a new account
4
exports.showCreate = function(request, response) {
5
response.render('users/create', {
6
title: 'Create User Account',
7
// include any errors (success messages not possible for view)
8
errors: request.flash('errors'),
9
});
10
};
11
12
// create a new user based on the form submission
13
exports.create = function(request, response) {
14
const params = request.body;
15
16
// Create a new user based on form parameters
17
const user = new User({
18
fullName: params.fullName,
19
email: params.email,
20
phone: params.phone,
21
countryCode: params.countryCode,
22
password: params.password,
23
});
24
25
user.save(function(err, doc) {
26
if (err) {
27
// To improve on this example, you should include a better
28
// error message, especially around form field validation. But
29
// for now, just indicate that the save operation failed
30
request.flash('errors', 'There was a problem creating your'
31
+ ' account - note that all fields are required. Please'
32
+ ' double-check your input and try again.');
33
34
response.redirect('/users/new');
35
} else {
36
// If the user is created successfully, send them an account
37
// verification token
38
user.sendAuthyToken(function(err) {
39
if (err) {
40
request.flash('errors', 'There was a problem sending '
41
+ 'your token - sorry :(');
42
}
43
44
// Send to token verification page
45
response.redirect('/users/'+doc._id+'/verify');
46
});
47
}
48
});
49
};
50
51
// Display a form that allows users to enter a verification token
52
exports.showVerify = function(request, response) {
53
response.render('users/verify', {
54
title: 'Verify Phone Number',
55
// include any errors
56
errors: request.flash('errors'),
57
// success messsages
58
successes: request.flash('successes'),
59
// Include database ID to include in form POST action
60
id: request.params.id,
61
});
62
};
63
64
// Resend a code if it was not received
65
exports.resend = function(request, response) {
66
// Load user model
67
User.findById(request.params.id, function(err, user) {
68
if (err || !user) {
69
return die('User not found for this ID.');
70
}
71
72
// If we find the user, let's send them a new code
73
user.sendAuthyToken(postSend);
74
});
75
76
// Handle send code response
77
function postSend(err) {
78
if (err) {
79
return die('There was a problem sending you the code - please '
80
+ 'retry.');
81
}
82
83
request.flash('successes', 'Code re-sent!');
84
response.redirect('/users/'+request.params.id+'/verify');
85
}
86
87
// respond with an error
88
function die(message) {
89
request.flash('errors', message);
90
response.redirect('/users/'+request.params.id+'/verify');
91
}
92
};
93
94
// Handle submission of verification token
95
exports.verify = function(request, response) {
96
let user = {};
97
98
// Load user model
99
User.findById(request.params.id, function(err, doc) {
100
if (err || !doc) {
101
return die('User not found for this ID.');
102
}
103
104
// If we find the user, let's validate the token they entered
105
user = doc;
106
user.verifyAuthyToken(request.body.code, postVerify);
107
});
108
109
// Handle verification response
110
function postVerify(err) {
111
if (err) {
112
return die('The token you entered was invalid - please retry.');
113
}
114
115
// If the token was valid, flip the bit to validate the user account
116
user.verified = true;
117
user.save(postSave);
118
}
119
120
// after we save the user, handle sending a confirmation
121
function postSave(err) {
122
if (err) {
123
return die('There was a problem validating your account '
124
+ '- please enter your token again.');
125
}
126
127
// Send confirmation text message
128
const message = 'You did it! Signup complete :)';
129
user.sendMessage(message, function() {
130
// show success page
131
request.flash('successes', message);
132
response.redirect(`/users/${user._id}`);
133
}, function(err) {
134
request.flash('errors', 'You are signed up, but '
135
+ 'we could not send you a message. Our bad :(');
136
});
137
}
138
139
// respond with an error
140
function die(message) {
141
request.flash('errors', message);
142
response.redirect('/users/'+request.params.id+'/verify');
143
}
144
};
145
146
// Show details about the user
147
exports.showUser = function(request, response, next) {
148
// Load user model
149
User.findById(request.params.id, function(err, user) {
150
if (err || !user) {
151
// 404
152
return next();
153
}
154
155
response.render('users/show', {
156
title: 'Hi there ' + user.fullName + '!',
157
user: user,
158
// any errors
159
errors: request.flash('errors'),
160
// any success messages
161
successes: request.flash('successes'),
162
});
163
});
164
};
165

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

1
const User = require('../models/User');
2
3
// Display a form that allows users to sign up for a new account
4
exports.showCreate = function(request, response) {
5
response.render('users/create', {
6
title: 'Create User Account',
7
// include any errors (success messages not possible for view)
8
errors: request.flash('errors'),
9
});
10
};
11
12
// create a new user based on the form submission
13
exports.create = function(request, response) {
14
const params = request.body;
15
16
// Create a new user based on form parameters
17
const user = new User({
18
fullName: params.fullName,
19
email: params.email,
20
phone: params.phone,
21
countryCode: params.countryCode,
22
password: params.password,
23
});
24
25
user.save(function(err, doc) {
26
if (err) {
27
// To improve on this example, you should include a better
28
// error message, especially around form field validation. But
29
// for now, just indicate that the save operation failed
30
request.flash('errors', 'There was a problem creating your'
31
+ ' account - note that all fields are required. Please'
32
+ ' double-check your input and try again.');
33
34
response.redirect('/users/new');
35
} else {
36
// If the user is created successfully, send them an account
37
// verification token
38
user.sendAuthyToken(function(err) {
39
if (err) {
40
request.flash('errors', 'There was a problem sending '
41
+ 'your token - sorry :(');
42
}
43
44
// Send to token verification page
45
response.redirect('/users/'+doc._id+'/verify');
46
});
47
}
48
});
49
};
50
51
// Display a form that allows users to enter a verification token
52
exports.showVerify = function(request, response) {
53
response.render('users/verify', {
54
title: 'Verify Phone Number',
55
// include any errors
56
errors: request.flash('errors'),
57
// success messsages
58
successes: request.flash('successes'),
59
// Include database ID to include in form POST action
60
id: request.params.id,
61
});
62
};
63
64
// Resend a code if it was not received
65
exports.resend = function(request, response) {
66
// Load user model
67
User.findById(request.params.id, function(err, user) {
68
if (err || !user) {
69
return die('User not found for this ID.');
70
}
71
72
// If we find the user, let's send them a new code
73
user.sendAuthyToken(postSend);
74
});
75
76
// Handle send code response
77
function postSend(err) {
78
if (err) {
79
return die('There was a problem sending you the code - please '
80
+ 'retry.');
81
}
82
83
request.flash('successes', 'Code re-sent!');
84
response.redirect('/users/'+request.params.id+'/verify');
85
}
86
87
// respond with an error
88
function die(message) {
89
request.flash('errors', message);
90
response.redirect('/users/'+request.params.id+'/verify');
91
}
92
};
93
94
// Handle submission of verification token
95
exports.verify = function(request, response) {
96
let user = {};
97
98
// Load user model
99
User.findById(request.params.id, function(err, doc) {
100
if (err || !doc) {
101
return die('User not found for this ID.');
102
}
103
104
// If we find the user, let's validate the token they entered
105
user = doc;
106
user.verifyAuthyToken(request.body.code, postVerify);
107
});
108
109
// Handle verification response
110
function postVerify(err) {
111
if (err) {
112
return die('The token you entered was invalid - please retry.');
113
}
114
115
// If the token was valid, flip the bit to validate the user account
116
user.verified = true;
117
user.save(postSave);
118
}
119
120
// after we save the user, handle sending a confirmation
121
function postSave(err) {
122
if (err) {
123
return die('There was a problem validating your account '
124
+ '- please enter your token again.');
125
}
126
127
// Send confirmation text message
128
const message = 'You did it! Signup complete :)';
129
user.sendMessage(message, function() {
130
// show success page
131
request.flash('successes', message);
132
response.redirect(`/users/${user._id}`);
133
}, function(err) {
134
request.flash('errors', 'You are signed up, but '
135
+ 'we could not send you a message. Our bad :(');
136
});
137
}
138
139
// respond with an error
140
function die(message) {
141
request.flash('errors', message);
142
response.redirect('/users/'+request.params.id+'/verify');
143
}
144
};
145
146
// Show details about the user
147
exports.showUser = function(request, response, next) {
148
// Load user model
149
User.findById(request.params.id, function(err, user) {
150
if (err || !user) {
151
// 404
152
return next();
153
}
154
155
response.render('users/show', {
156
title: 'Hi there ' + user.fullName + '!',
157
user: user,
158
// any errors
159
errors: request.flash('errors'),
160
// any success messages
161
successes: request.flash('successes'),
162
});
163
});
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

1
const mongoose = require('mongoose');
2
const bcrypt = require('bcrypt');
3
const config = require('../config');
4
5
// Create authenticated Authy and Twilio API clients
6
const authy = require('authy')(config.authyKey);
7
const twilioClient = require('twilio')(config.accountSid, config.authToken);
8
9
// Used to generate password hash
10
const SALT_WORK_FACTOR = 10;
11
12
// Define user model schema
13
const UserSchema = new mongoose.Schema({
14
fullName: {
15
type: String,
16
required: true,
17
},
18
countryCode: {
19
type: String,
20
required: true,
21
},
22
phone: {
23
type: String,
24
required: true,
25
},
26
verified: {
27
type: Boolean,
28
default: false,
29
},
30
authyId: String,
31
email: {
32
type: String,
33
required: true,
34
unique: true,
35
},
36
password: {
37
type: String,
38
required: true,
39
},
40
});
41
42
// Middleware executed before save - hash the user's password
43
UserSchema.pre('save', function(next) {
44
const self = this;
45
46
// only hash the password if it has been modified (or is new)
47
if (!self.isModified('password')) return next();
48
49
// generate a salt
50
bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) {
51
if (err) return next(err);
52
53
// hash the password using our new salt
54
bcrypt.hash(self.password, salt, function(err, hash) {
55
if (err) return next(err);
56
57
// override the cleartext password with the hashed one
58
self.password = hash;
59
next();
60
});
61
});
62
});
63
64
// Test candidate password
65
UserSchema.methods.comparePassword = function(candidatePassword, cb) {
66
const self = this;
67
bcrypt.compare(candidatePassword, self.password, function(err, isMatch) {
68
if (err) return cb(err);
69
cb(null, isMatch);
70
});
71
};
72
73
// Send a verification token to this user
74
UserSchema.methods.sendAuthyToken = function(cb) {
75
var self = this;
76
77
if (!self.authyId) {
78
// Register this user if it's a new user
79
authy.register_user(self.email, self.phone, self.countryCode,
80
function(err, response) {
81
if (err || !response.user) return cb.call(self, err);
82
self.authyId = response.user.id;
83
self.save(function(err, doc) {
84
if (err || !doc) return cb.call(self, err);
85
self = doc;
86
sendToken();
87
});
88
});
89
} else {
90
// Otherwise send token to a known user
91
sendToken();
92
}
93
94
// With a valid Authy ID, send the 2FA token for this user
95
function sendToken() {
96
authy.request_sms(self.authyId, true, function(err, response) {
97
cb.call(self, err);
98
});
99
}
100
};
101
102
// Test a 2FA token
103
UserSchema.methods.verifyAuthyToken = function(otp, cb) {
104
const self = this;
105
authy.verify(self.authyId, otp, function(err, response) {
106
cb.call(self, err, response);
107
});
108
};
109
110
// Send a text message via twilio to this user
111
UserSchema.methods.sendMessage =
112
function(message, successCallback, errorCallback) {
113
const self = this;
114
const toNumber = `+${self.countryCode}${self.phone}`;
115
116
twilioClient.messages.create({
117
to: toNumber,
118
from: config.twilioNumber,
119
body: message,
120
}).then(function() {
121
successCallback();
122
}).catch(function(err) {
123
errorCallback(err);
124
});
125
};
126
127
// 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.

Need some help?

Terms of service

Copyright © 2024 Twilio Inc.