Authenticating Users with the Twilio Authy App and Verify in Next.js

April 08, 2024
Written by
Desmond Obisi
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Authenticating Users with the Twilio Authy App and Verify in Next.js

Securing your application and user's data against maliciousness is always the key concern of every user-driven business. The most common ways of tackling this problem are by using strong passwords, one-time passwords (OTP), educating users not to share their data with anyone, etc.

In this tutorial, you will learn how to implement Two Factor Authentication (2FA) in a Next.js application using the Twilio Authy app and the Verify API. This layer of security ensures that access to the application is by the user and authenticated with codes that are synced to their device through the Authy app.

Prerequisites

Here are some requirements you need to follow in this tutorial

Developing Your Application

To start developing your solutions, you’ll need a front-end application built with Next.js and you’ll also need to set up your Twilio connection to the app using your credentials. This section will guide you on how to get the Next.js app up and running in minutes and how to connect to Twilio through your credentials.

Setting up your Next.js app

First, you will need to clone the Next.js boilerplate created for this tutorial. It is a simple application with signup, login, dashboard, and logout features. The application connects to a MongoDB Atlas database using the provisioned URL. The registered users are stored in the database and retrieved upon login with the right credentials. You will only be adding two new features to the application namely - the Scan Token page and the Verify Code page which add an extra layer of security to the application using Twilio Authy and Twilio Verify API.

Clone the repository by running this command in the directory where you want the project to be and install all the packages that are needed to run.

git clone https://github.com/DesmondSanctity/twilio-authy.git
cd twilio-authy
npm install

This frontend boilerplate runs on Next.js v13.2.4 and uses Page Router. Using a newer version of Next.js or the App Router might need extra setup from your end. To avoid the extra setups, you can use the version used in the boilerplate code

To get your MongoDB Atlas URL for the database connection, head over to MongoDB’s website to log in if you already have an account or register if you are a new user.

This is the first screen you will see when you have successfully registered on MongoDB for the first time. Click on the “I’ll deploy my database later” button and head to the dashboard as shown below:

 

An image showing MongoDB Atlas tiers while creating account.

Your dashboard should look like the one below. Click on Create to set up your database.

MongoDB Atlas dashboard where you can create database clusters.

Fill in the options as shown below and proceed to create your cluster. This cluster will contain all the tables and their respective data for our application. Click on Create to continue.

MongoDB Atlas configuration screen where you select configurations for your cluster.

You will need to set access and authorization next. As shown below, MongoDB auto-generates username and password for your cluster. Kindly update them to what you want and save them using the Create User button. Add 0.0.0.0/0 to the IP Access List input and click Add Entry to save. This allows access to your MongoDB database from any IP. You can update this later from the Network Access tab in the dashboard if you want to give access to limited IPs.

MongoDB Atlas configuration where you choose authentication type and set the credentials.

Click on the Finish and Close button to finish your setup and navigate to the dashboard. From the Dashboard, we can get the link to connect to our database by clicking Connect.

MongoDB Atlas dashboard showing a cluster deployed.

You will get to see different options to connect to your MongoDB database. In the tutorial, you will be using the Drivers which help you to access your data using MongoDB’s native drivers (e.g. Node.js, Go, etc.).

Connection options to connect to your Atlas cluster.

When you select the Drivers option, you will see the page with the credentials you need. It is a URL with a placeholder for your cluster password for authentication. Copy this URL for the next section.

Connection options for using MongoDB Node.js driver.

Create a .env file in the root directory of your project and set the following variables in it.

MONGODB_URI=<MongoDB URL>
JWT_SECRET=<JWT Secret Keys>
accountSid=<Twilio Account SID>
authToken=<Twilio Auth Token>
serviceSid=<Authy Service SID>

For the MONGODB_URI, paste in the URL from the previous section and replace the placeholder <password> with the password you added to the user when you configured the cluster.

The JWT_SECRET is a secret string used to tokenize the user’s access through jsonwebtoken. Replace the <JWT Secret Keys> placeholder with any string but for security reasons make it longer, alphanumeric, and difficult to guess.

Setting up a Twilio account

To set up your Twilio account, sign up for an account and login into your Twilio Console using your account details. From the toolbar's Account menu, select API Keys and Tokens. Take note of your test credentials as shown in the photo below, which include the Account SID and Auth Token, you will use them later in your application.

This photo shows the API keys from Twilio console

After getting your authentication tokens, you will create a Verify service from the dashboard. Click on Explore Products from the toolbar’s menu, click Verify, select Services, and click on the Create button to create one. You will get a screen  to fill in your service name, select the channels, and add some descriptive notes for the service. In this tutorial, you will select all the verification channels.

Click on Continue after that and select Yes for the Enable Free Guard section. At this point, your service is created and you will be redirected to a screen like the one below to update any settings and save. For this tutorial, you do not have to update any of the settings. Just save the settings and your setup is ready.

Setting the service configurations from the console

Finally, you will take note of the Service SID which you will use in your project to call this Authy service. You will find it in the next interface after saving the settings. Add your serviceSid  to the .env file.

Adding 2FA with Twilio Authy

In this section, you will create the API, services, and pages that will allow our frontend to use Twilio Authy for two-factor authentication. You will be calling the Twilio Verify API to create a new factor, verify a new factor, and create a challenge after the initial authentication setup by the users. You will also set up the pages for creating a scannable barcode and another for inputting the codes generated by the Authy mobile application. This will make up the full user authentication system you are building.

Creating the API functions

You will be adding new functions for the authentication and creating extra pages as well.

In the helpers/api/user-repo folder, you will add the following functions below the existing code and update the exportable variable usersRepo to export them as well. These codes will serve as the base for your Authy API services.

// update this userRepo exportable variable to contain the new functions below
export const usersRepo = {
 authenticate,
 getAll,
 getById,
 create,
 update,
 delete: _delete,
 createFactor,
 verifyNewFactor,
 createChallenge,
};


// add these functions below the existing ones in the file
async function createFactor({ name, identity }) {
 const user = await User.findById(identity);
 return await client.verify.v2
  .services(serviceSid)
  .entities(identity)
  .newFactors.create({
   friendlyName: `${name}'s TOTP`,
   factorType: 'totp',
  })
  .then(async (new_factor) => {
   // copy params properties to user
   Object.assign(user, {
    keys: new_factor.binding,
    factorSid: new_factor.sid,
   });
   await user.save();
   return new_factor;
  });
}


async function verifyNewFactor({ identity, code }) {
 const user = await User.findById(identity);
 return await client.verify.v2
  .services(serviceSid)
  .entities(identity)
  .factors(user?.factorSid)
  .update({ authPayload: code })
  .then(async (factor) => {
   if (factor.status === 'verified') {
    // copy params properties to user
    Object.assign(user, {
     authenticated: true,
    });
    await user.save();
   }
   return factor;
  });
}




async function createChallenge({ identity, factorSid, code }) {
 return await client.verify.v2
  .services(serviceSid)
  .entities(identity)
  .challenges.create({
   authPayload: code,
   factorSid: factorSid,
  })
  .then((challenge) => {
   return challenge;
  });
}
  • The createFactor function creates the TOTP factor using the user’s name and unique identity. It will in response return the sid, the binding URI, and other key parameters that will be used to recognize that user anytime they log in to the application.

  • The verifyNewFactor function verifies the new factor created by the first function. The binding URI is used to generate a QR code which the user scans to get the authentication codes. The verifyNewFactor accepts the unique identity and the code from the Authy app to verify the factor created initially. In response, it returns the status, whether verified or not verified.

  • The createChallenge function is used for continuous logging into the application. Since signing in requires 2FA, this function will verify the codes from the app when a user inputs one. It takes in a unique identity, the factorSid, and the code from the app and in response returns a status whether it is approved or not.

Next, you will write the API functions in the pages/api/code directory. In this directory, you will create three files: create.js, verify.js, and challenge.js. These files will hold the API invocation functions that will be used in the app services.

If you clone the starter project, the pages/api/code directory does not exist and you will need to create the directory before adding the files above.

Update the files with the following code below:

// pages/api/code/create.js


import { apiHandler, usersRepo } from "helpers/api";


export default apiHandler({
  post: createFactor,
});


async function createFactor(req, res) {
  const response = await usersRepo.createFactor(req.body);
  return res.status(200).json(response);
}
// pages/api/code/verify.js


import { apiHandler, usersRepo } from "helpers/api";


export default apiHandler({
  post: verifyNewFactor,
});
async function verifyNewFactor(req, res) {
  const response = await usersRepo.verifyNewFactor(req.body);
  return res.status(200).json(response);
}
// pages/api/code/challenge.js


import { apiHandler, usersRepo } from "helpers/api";


export default apiHandler({
  post: createChallenge,
});


async function createChallenge(req, res) {
  const response = await usersRepo.createChallenge(req.body);
  return res.status(200).json(response);
}

Inside these individual files, you are importing two helpers API functions, the apiHandler and the userRepo which have the functions for each task: create, verify, and create challenges. You will then create an API function and pass it to the apiHandler with the HTTP method specified. This setup helps you to utilize Next.js API functionality to call these functions as endpoints. For example: api/code/create will be an endpoint to create a new factor.

Creating the app services

The next step is to utilize the API in a service. These services will hit the endpoints where those API functions reside and the API functions will call the helper functions. The response received is what you will use to decide if the user passed the 2FA or not.


In the services/user.service.js file, you will update the userService variable and add the following functions as shown below:

export const userService = {
  user: userSubject.asObservable(),
  get userValue() {
    return userSubject.value;
  },
  login,
  logout,
  register,
  getAll,
  getById,
  update,
  delete: _delete,
  createFactor,
  verifyNewFactor,
  createChallenge,
};

async function createFactor(name, identity) {
  const response = await fetchWrapper.post(`${baseUrl}/code/create`, {
    name,
    identity,
  });
  return response;
}

async function verifyNewFactor(identity, code) {
  const response = await fetchWrapper.post(`${baseUrl}/code/verify`, {
    identity,
    code,
  });

  const appData = JSON.parse(localStorage.getItem("appData"));
  // Set auth to true
  appData.auth = true;
  // Save updated appData
  localStorage.setItem("appData", JSON.stringify(appData));

  // publish updated appData to subscribers
  userSubject.next(appData);

  return response;
}

async function createChallenge(identity, factorSid, code) {
  const response = await fetchWrapper.post(`${baseUrl}/code/challenge`, {
    identity,
    factorSid,
    code,
  });

  const appData = JSON.parse(localStorage.getItem("appData"));
  // Set auth to true
  appData.auth = true;
  // Save updated appData
  localStorage.setItem("appData", JSON.stringify(appData));

  // publish updated appData to subscribers
  userSubject.next(appData);

  return response;
}
  • The createFactor function calls the /code/create API endpoint and passes the needed parameters: name and the unique identity to it. The API handles the request and returns a response

  • The verifyNewFactor function calls the /code/verify API endpoint, and passes the needed parameters: identity and the code from the Authy app. The API handles the request and when there is a successful response, you update the local storage where you keep the user authentication state and other user details.

  • The createChallenge function calls the /code/challenge API endpoint anytime a user tries to log in. It passes the needed parameters: identity, factorSid, and the code from the Authy app. When there is a successful response, you update the local storage as well with the authentication state.

  • The fetchWrapper helper function helps the API to run the request, checks for authentication where necessary, and catches error responses.

Adding new UI for 2FA

In this section, you will create additional user interfaces in the application to scan the authentication key using the Twilio Authy app. These screens will now be part of the login/register flow.

The initial flow of the app is: Register a user → Login the user → Dashboard. You will update this flow to be: Register a user → Login the user → Generate/Scan the QR Code → Verify Code → Dashboard. Subsequently, registered users will not generate or scan QR codes to log in, they will only verify 2FA codes after inputting username and password before getting to the dashboard. This is the end goal of this guide.

In the pages/account/login.js file, you will update the onSubmit function on line 26 to the code below:

 

 function onSubmit({ username, password }) {
    alertService.clear();
    return userService
      .login(username, password)
      .then(() => {
        if (userService.userValue.user.authenticated) {
          router.push("code");
        } else {
          router.push("scan");
        }
      })
      .catch(alertService.error);
  }

This function makes sure it routes you to the correct next screen. If the users have already authenticated the first time, they should not scan the QR code again, they should just put the 2FA code from the Authy app in the code user interface.

You will create two new files in the pages/account directory named scan.js and code.js respectively. You will add the following code to the files as shown below. Due to the length of the code, I will be adding the GitHub gist link instead.

scan.js

https://gist.github.com/DesmondSanctity/c953d84c8f6d77db5e686bd716ce3926

code.js

https://gist.github.com/DesmondSanctity/b3f334bcea562582c9aa4ecf1ff01f54

Testing and Product Demonstration

Your app is now ready for testing. You can start the local development server by running this command:

npm run dev

Once the app runs, you can access it on the browser at  http://localhost:3000. To make sure it works well in a live environment, you will use ngrok to generate a live server for your app by running this command:

ngrok http http://localhost:3000

A live server will be created by ngrok as a proxy to your localhost server as shown below:

Console after executing ngrok command

You can now open the ngrok URL in your browser and follow the signup/login flow to do your two-factor authentication. This is accomplished by generating a scannable QR code in the app on login and verifying it with the Twilio Authy app. This flow is also demonstrated in the video below:

Twilio Authy + Next.js Frontend Demo

Conclusion

You have successfully learned how to implement MFA or 2FA to your applications using the Twilio Verify API and the Authy app. This is a recommendable idea for app owners who want to level up the security of their applications and reduce account hacking or exploitations. You can learn more about the Verify API and how it will help your applications or businesses by referring to Twilio’s documentation.

Desmond Obisi is a software engineer and a technical writer who loves developer experience engineering. He’s very invested in building products and providing the best experience to users through documentation, guides, building relations, and strategies around products. He can be reached on Twitter, LinkedIn, or through email at desmond.obisi.g20@gmail.com.