How to Build Passwordless Auth Using TOTP With Twilio Verify in Node.js
Time to read:
How to Build Passwordless Auth Using TOTP With Twilio Verify in Node.js
Typing or generating a unique password for every new account or website is a hassle. Although password managers help, passwordless auth (authentication) offers a more streamlined and secure alternative.
In this tutorial, you will learn about passwordless auth, and build a Node.js app that uses Twilio Verify to implement it.
Prerequisites
Before you begin, ensure you have the following:
- A free Twilio account. Click here to create a free account if you are new to Twilio.
- An authentication app, such as Twilio Authy
- Node.js (ideally, version 20 or later) and npm
- Your favourite text editor or IDE (such as neovim or Visual Studio Code)
- Your favourite web browser
Architecture
This application will be a simplistic web-based application, split up over three stages.
- Stage one: the user will submit their username to begin setting up Two-factor Authentication (2FA).
- Stage two: they'll set up a TOTP entry in their authenticator app. They'll do this with their authenticator app by scanning a QR code or entering a unique code. Then, they'll submit the initial code that their authenticator app generates for them. If the code is valid, their account is ready to use.
- Stage three: They can validate future codes which their authenticator app generates for them in the final form in the application.
What is passwordless auth?
Passwordless auth is an authentication method where the user does not need a password in order to log into an app or system. Rather, the user's mobile device receives a one-time code. In this authentication method, users are authenticated using other unique and more secure alternatives like Time-based One-time Passwords (TOTP, or Soft Token), SMS, Passkeys, Silent Network Authentication (SNA), Voice, or email notification.
Here's a breakdown of how it works, using TOTP:
- User initiation: When a user creates an account, instead of entering a password, they create a new Factor resource with Twilio Verify — initially marked as unverified — seeding it with a unique (auto-generated) identifier.
- Scan the QR code with an authenticator app: Using an authenticator app such as Twilio Authy, they scan the QR code then enter the code that the authenticator app provides. The code is validated using Twilio Verify. If the code is valid the Factor is marked verified. The authenticator app can now provide time-based codes to use in the future, when logging in, which Twilio Verify will verify.
- Code validation: When the user logs in, they enter their username and a TOTP code generated by their authenticator app. The application then uses Twilio Verify to validate the code along with their unique identifier.
This approach significantly enhances security by leveraging the inherent security features of the user's mobile device as a second factor in the authentication process. It also eliminates the need for them to memorize passwords, thereby simplifying the authentication process and enhancing user convenience.
Passwordless authentication has additional advantages, including the following:
- Enhanced security: Reduces the risk of password-related breaches.
- Convenience: Eliminates the need for users to remember and manage multiple passwords.
- Reduced friction: Streamlines the login process, improving the user experience.
- Scalability: Easily scalable with Twilio's infrastructure.
Build the app
Step 1: Set up the project
Set up a new project by running the following commands, where you store your Node.js projects:
The npm init -y command creates a package.json file with npm's defaults, so you don't have to answer the interactive prompts. The mkdir command creates the three directories that this project needs: src for the application code, views for the HTML templates, and public/css for the stylesheet.
Now, open the project directory in your preferred text editor or IDE, and open package.json. Add a start script to the scripts section, so that you can start the app with npm start:
Step 2: Install the required dependencies
Next, install the required dependencies, by running the following:
In case you're not familiar with them, here's a short description of the dependencies that you just installed:
- dotenv: Loads environment variables from .env into
process.envautomagically. - EJS: This is the templating engine that renders the application's HTML pages, using plain JavaScript inside the templates.
- Express: This is the web framework that handles the application's routing and requests.
- express-session: Session middleware for Express, which stores each user's data between requests.
- node-qrcode: This simplifies generating QR codes in the application
- Twilio's Node.js Helper Library: This simplifies integrating with Twilio in Node.js
Step 3: Set the required environment variables
Dotenv files (commonly named .env) are used to store the configuration information that your app needs during development, separate from the application's code. For this tutorial, it will be your Twilio credentials (i.e., your Twilio Account SID and Auth Token), a Verify Service SID, and a secret used to sign the session cookie.
In your project's top-level directory, create a new file named .env and add the following variables to it.
Set SESSION_SECRET to any long, random string. Express uses it to sign the session cookie so that it can't be tampered with.
You next need to retrieve the credentials to set as the values of the other three variables. To do that, sign into the Twilio Console. There, click the black and white up arrow at the bottom of the page, and you should see your Account SID and Auth Token in the Workbench; as shown in the image below.
Copy these values and paste them into .env as the values for TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN.
Next, navigate to Products and Services > Verify > Services. On this page, click Create new. Then, fill out the initial form with the configuration values shown in the screenshot below, and click Continue.
In the next step, leave Enable Fraud Guard set to "Yes" and click Continue to finish creating the service.
After creating the service, copy the Service SID and paste it into .env as the value of TWILIO_VERIFY_SERVICE_SID.
Step 4: Build the base Node.js application
The next thing to do is to load the environment variables and set up the Express server. To do that, create a new file named index.js in the project's top-level directory, and add the code below to it:
The code above starts by loading the environment variables defined in .env, using dotenv. Passing quiet: true to config() stops dotenv from printing a summary banner every time the app starts.
The order of the statements matters here. require('dotenv').config() and the check both run before the require() calls below them, because Node evaluates a module's statements from top to bottom. That way, src/routes.js — which you'll write next — can read process.env as soon as it loads.
After that, the code creates the Express app and registers its middleware, in the order that each request passes through it:
express.static()serves anything in the public directory as-is, which is how the stylesheet gets to the browserexpress.urlencoded()parses submitted form data intoreq.bodysession()gives each visitor a session, stored inreq.session. The rolling option refreshes the session cookie on every response, andmaxAgeexpires it after one hourroutesis the application's own routes, which you'll write next
Finally, it starts listening on the port set in the PORT environment variable, falling back to port 8080.
With that done, create a new file named routes.js in the src directory, and add the following code to it:
This file creates a Twilio REST client from your credentials, which will be used when creating and verifying TOTP codes. Because Node caches a module after the first time it's required, the client is created once and shared by every request.
It also narrows the client down to your Verify Service once, as verifyService, so that each route handler doesn't have to repeat the service SID.
Then it creates an Express router and exports it. A router is a group of routes that you attach to the app, which is how the route handlers stay out of index.js. You'll add a route to it in each of the next three steps.
Step 5: Add the ability to create a new TOTP Factor
Now, you'll add the first feature of the application: the ability to create a new TOTP Factor. To do that, add the following route to src/routes.js, between the router declaration and the module.exports line at the bottom of the file. Every route in the following steps goes in that same place, one after another.
This handler uses EJS to render views/enter-username.ejs. It will be called in response to GET requests to the application's default route "/", rendering a form for the user to enter their username as the first stage in the TOTP setup process.
Now, in the views directory, create a new file named enter-username.ejs, and in that file, paste the code below:
As you can see from the HTML above, it is a simplistic HTML page with a form, with a single field named "username". Note that the field's type and inputmode attributes are set to "text". This hints to browsers on mobile devices to render a virtual keyboard most appropriate for entering text input.
Then, back in src/routes.js, add the following POST route after the GET route that you added at the start of this step.
This handler creates a new TOTP Factor with Twilio Verify. The factor is seeded by 16 random bytes generated with crypto.randomBytes(), converted to hex. Each byte becomes two hex characters, so the result is always 32 characters long — comfortably inside the 8 - 64 character limit that Verify enforces on the identity.
Following this, the seed is used to create a new factor resource using the Twilio REST client. The seed, along with the following properties from the response, are then stored in session, because the later stages of the flow need them:
- Friendly Name: The Factor's friendly name
- SID: A 34 character string that uniquely identifies the Factor.
- OTP URI: This stores the OTP configuration, including a shared secret and related parameters, for the OTP client (e.g., Authy) to use to generate the initial TOTP code.
Then, if the new Factor's status is set to "unverified", the user is redirected to "/challenge" to verify it. Otherwise, they're redirected back to "/" to try again. There's no status code passed to res.redirect(), because it sends a 302 by default.
Note that the handler is declared async, because every method on the Twilio helper library returns a promise. Express 5 forwards a rejected promise from a route handler to its error handler for you, so you don't need to wrap the call in a try/catch block to avoid an unhandled rejection.
Step 6: Add the ability to verify the TOTP Factor
The two routes you're about to add both need the Factor details that the previous step stored in session. If someone opens either one directly — with an expired session, or before creating a Factor — there's nothing to verify, so the app should send them back to the start.
Rather than repeat that check inside each handler, write it once as middleware. Add the following function to src/routes.js, after the router declaration and before the routes:
A middleware function receives the request, the response, and next. Calling next() passes the request along to the handler behind it; returning without calling it ends the request there, which is what the redirect does. You'll pass requireFactor to each of the remaining routes as a second argument, so that Express runs it before the handler.
Now, add the functionality to render the form with the QR code for verifying the new Factor resource, by adding the code below to src/routes.js, after the route you added at the end of the last step.
This handler renders views/verify-user.ejs, providing the form for the user to enter to validate their new TOTP Factor. The template will render two variables:
- A QR code which embeds the unverified Factor's OTP URI
- The unverified Factor's seed or identity
QRCode.toDataURL() returns the QR code as a data URL, which can be set directly as the src of an <img> element.
With that done, in the views directory create a new file named verify-user.ejs and paste the code below into the file.
The HTML renders another, simplistic, form with the QR code to scan, and a field for the TOTP code generated by the user's authenticator app. The field uses the pattern attribute to ensure that the only valid input is a 6-digit code. It also sets the inputmode attribute to "numeric" to have mobile browsers display a keyboard appropriate for entering digits, making it easier for the user to only enter numeric input.
Note that the <%= %> tags escape their contents before writing them into the page, which is what you want for values that came from user input or an API response.
Next, back in src/routes.js, add the following route after the one you added at the start of this step.
This handler retrieves the code that the user submitted from the request's POST data and attempts to verify the new Factor using it, along with the Factor's seed and SID, retrieved from the current session.
If the Factor isn't verified, they're redirected back to the verify TOTP Factor stage, to scan the QR code and try again. Otherwise, a confirmation message is flashed and the user is redirected to the route where they can enter codes generated by their authenticator app post-Factor creation, where the flashed message will be displayed, confirming that the Factor was successfully verified.
The confirmation is written to req.session.message. Because a redirect sends the browser off to make a second, separate request, a message can't be passed to the next page in a local variable — it has to be stored somewhere that outlives the current request, and the session is already there. The next step reads the message back out and deletes it, so it appears once and doesn't linger on later page loads.
Step 7: Add the ability to validate TOTP codes after Factor verification
Now, you'll add the third and final feature: the ability to validate TOTP codes, post-Factor verification. Start by adding the following two routes to src/routes.js, after the route you added at the end of the last step.
The first renders an HTML form where the user can enter and submit a TOTP code generated by their authenticator app. The second verifies the code with Twilio Verify.
If Twilio Verify marks the code as "approved", then a message confirming that is stored in the session. Otherwise, a message confirming that the code was invalid is stored instead. The user is then redirected back to the TOTP code form, where the message will be displayed.
The GET handler reads that message and then deletes it from the session, so it's shown exactly once — reloading the page afterwards displays the form with no message above it. It falls back to null when there's nothing to show, because EJS throws a ReferenceError if a template refers to a variable that wasn't passed to it.
Now, in views create a new file named enter-code.ejs, and paste the code below into the file.
Similar to the previous template, this one renders a form with a field where the user can input and submit the code which their authenticator app generates. It also provides a link to start over and create a new Factor.
Step 8: Download the application's CSS file
The last step in the process is to download the application's CSS file from the project's GitHub repository, to the project's public/css directory, naming it styles.css. You can do that by running the following command in the project's top-level directory.
Test that the application works
Finally, it's time to test that the code works as expected. Start the application by running the following command.
Your server will start on port 8080, as shown by terminal output similar to the following.
You can now navigate to http://localhost:8080 to test the TOTP Factor creation process.
Enter a username of your choice and click Set up two-factor authentication.
You will be redirected to the Verify New TOTP Factor form. With your authentication app, scan the QR code, enter the 6-digit code that it generates into the form, and click Verify.
You'll then be redirected to the Enter TOTP Code form, where you'll see whether the Factor was set up successfully or not, as in the screenshot above.
If the Factor was set up successfully, enter the next 6-digit code into the Enter TOTP Code form and click Verify. Again, you should see if it was successfully verified or not, as in the screenshot above.
That's the essentials of implementing passwordless authentication in Node.js using Twilio Verify
TOTP-based passwordless authentication using Node.js and Twilio Verify presents a compelling alternative to traditional password-based systems.
It offers a blend of enhanced security and user convenience, making it an attractive option for modern applications. While passwordless auth introduces some extra complexity to your application, the benefits — especially regarding security and user experience — are worth the investment.
Dhruv Patel is a Developer on Twilio's Developer Voices team. You can find Dhruv working in a coffee shop with a glass of cold brew or he can be reached at dhrpatel [at] twilio.com .
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.