The how and why of 2FA using Twilio and Feathers.js — Simple 2FA tutorial

Harry Blakiston Houston
12 min readJun 15, 2018

--

For many services, fake accounts allow trolls and bots to takeover platforms sometimes causing damage to our societies on both a political and social level. One need only look as far as Twitter and Facebook for prime examples 9–15% of Twitter users are bots and Facebook has over 270,000,000 fake accounts!

Adding mobile verification can help protect networking services and utilities against bots.

Over the last couple of months, I’ve been working to build the backend for a project that will give people far greater control over their personal information. As with many products verification is an important feature. In the great word of Forrest Gump FeathersJS and Twilio (TwilioHouse) are like ‘peas’ and ‘carrots’ when it comes to implementing 2FA in javaScript (JavaScript Daily).

When doing this I worked from the brilliant tutorial written by Jon Paul Miles, and have merely tweaked his work to bring it up to date and focus on using SMS as my transport service. I would love to have your feedback on how I might improve this work or improve the article, for now, though writing on the day of the birth of Elizabeth Regina please do keep calm and carry on.

Overview of how the process works

Here an example of the flow when confirming a new user’s email.

  • The user creates an account.
  • Server sets a verifyToken on that user and sends the user a text with a verifyToken.
  • The user reads the code in the text the user is then able to verify using the verifyToken.
  • The server will compare the verifyToken to the verifyToken sent by the user, if they match it confirms that the user’s phone is valid.

For resetting a password the process is similar

  • User requests to reset password.
  • Server sets a verifyResetToken on the user and sends a text to the user with the verifyResetToken.
  • The client sends the password update and the verifyResetToken to the server.
  • Server compares verifyResetToken to the verifyResetToken the user-provided and if they match will encrypt the password and save it to the user.

For updating the identifiable information

  • User requests an information change sending their password in the request.
  • The server checks that the password is correct and if so sends a verifyToken via SMS to the user.
  • The user sends the verifyToken to the survey to confirm the information change.
  • The server compares verifyToken to the verifyToken the user-provided and if they match will save the confirm and save the changes.

What we are aiming to achieve:

  • An SMS sent to the user to confirm sign up
  • An SMS sent after confirming the account thanking the user
  • A corresponding hook that won’t give the user access unless they are confirmed.
  • Sending a reset password SMS
  • Sending a verify changes when the user tries to change their identities such as phone, or username. The changes will not be confirmed until the user has verified the changes with a shortToken.
  • An SMS sent when a password is changed.

Steps involved

  1. Set up the project.
  2. Generate a users service.
  3. Generate authentication.
  4. Generate our transport.
  5. Update transport hooks.
  6. Twilio stuff.
  7. Set our environment variables.
  8. Update sms service.
  9. Update users schema.
  10. Install feathers-auth-management.
  11. Generate management service.
  12. Update the management service.
  13. Update config file.
  14. Create send sms global hook.
  15. Install authentication dependencies.
  16. Update the users hooks.
  17. Update our authManagment hooks.
  18. Create a notifier.js file.
  19. Add isVerified hook to authentication file
  20. ( For the shrewd among us ) How can I do this without getting charged? Twilio test credentials.
  21. ( For the heavy testers ) How can I set the Twilio short auth token to a set value to allow for integration tests.

1.Set up the Project

Create a new file , navigate into the file, use the feathers cli to generate a new app and install the feathers mongoose npm package:

terminal

$ mkdir feathersTwilioTutorial

$ cd feathersTwilioTutorial/

$ feathers generate app

2. Generate users service

Here we are creating our base service that will store the users credentials.

terminal

$ feathers generate service? What kind of service is it? Mongoose? What is the name of the service? users? Which path should the service be registered on? /users? What is the database connection string? mongodb://localhost:27017/feathers_twilio_tutorial

3. Generate authentication

Feathers generate authentication is an easy way to add many different types of authentication to your product. Today we’ll be focussing on local and jwt. Authentication will then allow us to check who a particular user is and ensure that the users email is a valid email.

terminal

$ feathers generate authentication

4. Generate transport

We will need a transport service to send messages. By creating a new service we one will be able to subsequently call the sms service from inside our API to send messages.

terminal

$ feathers generate service? What kind of service is it? A custom service? What is the name of the service? sms? Which path should the service be registered on? /sms? Does the service require authentication? Nocreate src/services/sms/sms.service.js

5. Update transport hooks

Feathers hooks commons provide a couple of particularly useful functions. In this case dissallow(‘external’) is used to stop any external requests being accepted by the service.

terminal

$ npm install feathers-hooks-common -save

/src/services/sms/sms.hooks.js

const { disallow } = require(‘feathers-hooks-common’);module.exports = {before: {all: [disallow(‘external’)],find: [],get: [],create: [],update: [],patch: [],remove: [],},

6. Get Twilio details and install Twilio and feathers Twilio

At this point yes you’ll need to sign up for Twilio they provide a great service and great documentation so I’m sure you’ll enjoy it. Then use terminal to add Twilio dependencies. (N.B. If you want to know how to use Twilio for free via your test credentials scroll to the bottom section)

terminal

$ npm install twilio -save

$ npm install feathers-twilio -save

7. Set environment variables

Reading private information from your local environment is good practice in securing your private keys in case your project goes open-source or into a production environment.

terminal

export TWILIO_SID=your twilio SID
export TWILIO_AUTH_TOKEN=your twilio auth token
export TWILIO_NUMBER=your twilio number

8. Update SMS service

Update the SMS service to use the feathers-twilio service you must pass in your accountSid and authToken.

src/services/sms/sms.service.js

// Initializes the `sms` service on path `/sms`const hooks = require('./sms.hooks');const smsService = require(‘feathers-twilio/lib’).sms;const accountSid = process.env.TWILIO_SID;const authToken = process.env.TWILIO_AUTH_TOKEN;module.exports = function (app) {const paginate = app.get(‘paginate’);const options = {name: ‘sms’,paginate,accountSid,authToken,};// Initialize our service with any options it requiresapp.use(‘/sms’, smsService(options));// Get our initialized service so that we can register hooks and filtersconst service = app.service(‘sms’);service.hooks(hooks);};

9. Update users schema

Update the users model to include information relevant to the service make sure the phone number/username is unique. (You don’t actually need a username if you do want to have one).

src/models/users.model.js

// users-model.js — A mongoose model// See http://mongoosejs.com/docs/models.html// for more of what you can do here.module.exports = function (app) {const mongooseClient = app.get(‘mongooseClient’);const { Schema } = mongooseClient;const users = new Schema({phone: { type: String, unique: true },// phone number -> uniqueusername: { type: String, unique: true },// username -> uniquepassword: { type: String }, // user passwordisVerified: { type: Boolean },// Checks if user is verifiedverifyShortToken: { type: String },// Stores required short tokenverifyExpires: { type: Date },// Expires the tokenverifyChanges: { type: Object }, // Verify changes objectresetShortToken: { type: String },// Stores the rest tokenresetExpires: { type: Date },// Expires the reset token}, {timestamps: true,});return mongooseClient.model(‘users’, users);};

10. Install feathers-auth-management

Feathers auth management provides a range of useful functions.

terminal

$ npm install feathers-authentication-management -save

11. Generate auth management service

Use the CLI to generate a management service.

terminal

$ feathers generate service
? What kind of service is it? A custom service
? What is the name of the service? management
? Which path should the service be registered on? /management
? Does the service require authentication? No

12.Update the management service

This service will be used to configure managementwith the identifyUserProps the shortTokenLength and shortTokenDigits. The notifier will be created in step 18. We will use this endpoint for the majority of auth management requests.

src/services/management/management.service.js

const management = require(‘feathers-authentication-management’);// Initializes the `management` service on path `/management`const hooks = require(‘./management.hooks’);const notifier = require(‘./notifier’);module.exports = function (app) {const identifyUserProps = app.get(‘identifyUserProps’);const shortTokenLen = app.get(‘shortTokenLen’);const shortTokenDigits = app.get(‘shortTokenDigits’);const options = {identifyUserProps,shortTokenLen,shortTokenDigits,};// Initialize our service with any options it requiresapp.configure(management(options, notifier(app)));// Get our initialized service so that we can register hooks and filters// Get our initialized service so that we can register hooks and filtersconst service = app.service('management');service.hooks(hooks);};

13. Set config

Set the necessary variables in your config file the app will read this when for example app.get(‘identifyUserProps’); is invoked as in step 12.

/config/default.json

“identifyUserProps”:[“_id”,”phone”, “username”],“shortTokenLen”:6,“shortTokenDigits”:true,..."authentication": {..."local": {"entity": "user","usernameField": "username", // or phone whichever you prefer maybe both??"passwordField": "password"}}

14. Create send verification SMS global hook

This hook will be used to send a verification SMS to the user after the user has signed up (hence it is used in the after create users hook).

/src/hooks/sendVerificationSms.js

const accountService = require(‘../services/management/notifier’);const _ = () => (hook) => {if (!hook.params.provider) { return hook; }const user = hook.result;if (hook.data && hook.data.phone && user) {accountService(hook.app).notifier(‘resendVerifySignup’, user);return hook;}return hook;};module.exports = _;

15. Install authentication dependencies

Feathers authentication, authentication-local and authentication-jwt are three packages that will allow us to easily add authenticate via user credentials or a jwt. Authentication will be used to stop the user from accessing particular services such as DELETE user. A user will also only be able to authenticate and retrieve their jwt once they have verified their phone. (the jwt will help avoid CSRF attacks)

terminal

$npm install @feathersjs/authentication --save
$npm install @feathersjs/authentication-local --save
$npm install @feathersjs/authentication-jwt --save

16. Update users hooks

There is quite a lot going on in this service.js file.

so …

  1. verifyHooks.addVerification adds the required verification information to our user object this information can then be saved to the db.
  2. authenticate(‘jwt’) the user can only retrieve their jwt once they have verified their email the jwt then gets passed with each subsequent request authorising the user on any given service.
  3. hashPassword() ensures that the password is encrypted before being saved.
  4. commonHooks.disallow('external') stops the endpoint from being accessed by external requests.
  5. On before the patch service we firstly authenticate the user via jwt then we check if the request comes from an external source if so we stop them from updating any protected information.
  6. On the remove service we simply ensure that it is an authenticated user who is making the request.
  7. On all after services we sanitize the response ensuring that no protected data is sent to the client.
  8. On the create service after hook we call the send verificationSmsHook that we created before and remove the verificationInformation from the response object

/src/services/users/users.service.js

const { authenticate } = require('@feathersjs/authentication').hooks;const commonHooks = require('feathers-hooks-common');const verifyHooks = require('feathers-authentication-management').hooks;const sendVerificationSms = require('../../hooks/sendVerificationSms');const { hashPassword, protect } = require('@feathersjs/authentication-local').hooks;module.exports = {before: {all: [],find: [authenticate('jwt')], // stops user accessing the serviceget: [],create: [verifyHooks.addVerification(),hashPassword(),],update: [commonHooks.disallow('external')],patch: [authenticate('jwt'),commonHooks.iff(commonHooks.isProvider('external'),commonHooks.preventChanges('phone',
'username',
'isVerified','verifyToken','verifyShortToken','verifyExpires','verifyChanges','resetToken','resetShortToken','resetExpires',),),],remove: [authenticate('jwt')],},after: {all: [protect('password', '_computed', 'verifyExpires', 'resetExpires', 'verifyChanges')],find: [],get: [],create: [sendVerificationSms(),verifyHooks.removeVerification(),],update: [],patch: [],remove: [],},error: {all: [],find: [],get: [],create: [],update: [],patch: [],remove: [],},};

17. Update Auth-Managment hooks

The isAction function allows us to check what the action being performed is and if it is password change or identity change then the user must provide a valid jwt.

/services/management/management.hooks.js

const { authenticate } = require(‘@feathersjs/authentication’).hooks;const commonHooks = require(‘feathers-hooks-common’);const isAction = () => {const args = Array.from(arguments);return hook => args.includes(hook.data.action);};module.exports = {before: {all: [],find: [],get: [],create: [commonHooks.iff(isAction(‘passwordChange’, ‘identityChange’),[authenticate(‘jwt’),],),],update: [],patch: [],remove: [],},after: {all: [],find: [],get: [],create: [],update: [],patch: [],remove: [],},error: {all: [],find: [],get: [],create: [],update: [],patch: [],remove: [],},};

18. Create a file named notifier.js file in the auth-managment folder and add the following to it.

Then finally we need to make our notifier.js file. This file will be used to decide what to send the user depending on the input. We use a switch statement and inside each block we create the sms to be sent to the receiver. (I was a bit lazy with writing dry code here but I ain’t perfect or even close to it).

/src/services/management/notifier.js

const _ = function (app) {function sendSms(sms) {return app.service('sms').create(sms).then(() => {}).catch((err) => {throw Error(err);});}return {notifier: (type, user) => {let code;let sms;switch (type) {case 'resendVerifySignup': // send another sms with link for verifying user's email addresscode = user.verifyShortToken;sms = {from: process.env.TWILIO_NUMBER,to: user.phone,body: `Your verification code is: ${code}`,};break;case 'verifySignup': // inform that user's email is now confirmedcode = user.verifyShortToken;sms = {from: process.env.TWILIO_NUMBER,to: user.phone,body: `Your verification code is: ${code}`,};break;case 'sendResetPwd': // inform that user's email is now confirmedcode = user.verifyShortToken;sms = {from: process.env.TWILIO_NUMBER,to: user.phone,body: `Your reset code is: ${code}`,};break;case 'resetPwd': // inform that user's email is now confirmedcode = user.verifyShortToken;sms = {from: process.env.TWILIO_NUMBER,to: user.phone,body: `Your reset code is: ${code}`,};break;case 'passwordChange': // Inform the user of a password changesms = {from: process.env.TWILIO_NUMBER,to: user.phone,body: 'Your password was changed',};break;case 'identityChange': // Inform the user of an identity changecode = user.verifyShortToken;sms = {from: process.env.TWILIO_NUMBER,to: user.phone,body: 'Your password was changed',};break;default:break;}return sendSms(sms);},};};module.exports = _;

19. Check if the email is verified before authenticating the user

By adding the verification hook the user will only be able to refresh their jwt if they this current phone is verified. This means as long as you authenticate on all other methods using authenticate(‘jwt’). Unverified users will not be able to access any protected services.

/src/authentication.js...const verifyHooks = require('feathers-authentication-management').hooks;...app.service('authentication').hooks({before: {create: [authentication.hooks.authenticate(config.strategies),verifyHooks.isVerified(),],remove: [authentication.hooks.authenticate('jwt'),],},});

20. For the shrewd

By using your Twilio Test credentials allows one to ensure that they are not being charged for each service call. Simply replace your Number, AuthToken and SID for the test Test Number, Test Auth Token and Test SID (one can easily update this to another number if they would like to do negative tests).

terminal

export TWILIO_SID=your twilio test SID
export TWILIO_AUTH_TOKEN=your twilio test auth token
export TWILIO_NUMBER=+15005550006

21. For the heavy testers

!!!!!!!!Warning ensure you make a very clear note that you have done the following as you must return it to its old state before putting any product into production!!!!!!!

Replace the following text:

node_modules/feathers-authentication-management/lib/helpers

Approx line 76

...if (ifDigits) {// return Promise.resolve(_randomDigits(len));return Promise.resolve('1926') /* When i first did this this needed to be an integer but now it has changed to a string */}...

The requests

The collection requests can be viewed through the following postman link.

Please do send messages with any changes you wish to see in this tutorial. There may well be mistakes and I will try to quickly rectify them where they exist and include any updates that I have missed, I hope this tutorial has helped. Please feel free to connect with me on Twitter or LinkedIn. I will be publishing a range of tutorials over the coming months so would be great to hear about any particular interests that I might be able to help with. My plan for now is to continue with feathers and go deeper into the depths of Graph databases, they’ll be cool, exiting and good …. No great for both you and me.

Finally some thanks, of course David Luecke, Eric Kryski and all the rest of the guys and gals central to FeathersJS, I have loved working on what you guys have put together and of course Jon Paul Miles. I would not have got anywhere without your help. I hope one day to become a backer and I’ll look forward to seeing how Feathers progresses into the future.

‘Keep calm and carry on’

At Phonebox our mission is to give you greater control over your information, we are working tirelessly to achieve this goal and much appreciate any help and support we can get along the road. If you are interested in finding out more about what exactly we do or why we are unique do not hesitate to get in touch.

“Stupid is as stupid does”

--

--