---
"@context": https://schema.org
"@type": TechArticle
"@id": https://www.twilio.com/docs/authy/quickstart/two-factor-authentication-nodejs#article
headline: Authy Two-factor Authentication Node.js Quickstart
description: Authy guide for Node.js and Angular which protects a URL with Two-Factor Authentication. Includes SMS, Voice, Soft Tokens and Push Notification channels.
url: https://www.twilio.com/docs/authy/quickstart/two-factor-authentication-nodejs
inLanguage: en
dateModified: 2026-07-20T17:19:21.000Z
author:
  "@type": Organization
  name: Twilio Developer Education Team
publisher:
  "@type": Organization
  name: Twilio
---

# Authy Two-factor Authentication Node.js Quickstart

> \[!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](/docs/verify/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](https://www.twilio.com/blog/migrate-authy-to-verify).

Adding two-factor authentication (2FA) to your application is an effective way to increase security and trust in your product without unnecessarily burdening your users. This quickstart guides you through building a [Node.js](https://nodejs.org/en/), [AngularJS](https://angularjs.org/), and [MongoDB](https://www.mongodb.com/) application that restricts access to a URL. The app demonstrates four [Authy API channels](/docs/verify/authentication-channels): SMS, Voice, Soft Tokens, and Push Notifications.

## Sign Into - or Sign Up For - a Twilio Account

Create a new Twilio account (you can sign up for a [free Twilio trial](/try-twilio)), or sign into [an existing Twilio account](https://www.twilio.com/console).

### Create a New Authy Application

Once logged in, visit the [Authy Console](https://www.twilio.com/console/authy/applications). Click on the red 'Create New Application' (or big red plus ('+') if you already created one) to create a new Authy application then name it something memorable.

![Authy dashboard with message 'You have no Authy applications' and 'Create Application' button.](https://docs-resources.prod.twilio.com/029c875193dd2fda38477674df469f18c933db29907845bc69d1f67e5fcb1e2b.png)

You'll automatically be transported to the **Settings** page next. Click the eyeball icon to reveal your Production API Key.

![General settings page showing production API key with reveal option.](https://docs-resources.prod.twilio.com/dd7f3a45399b71568b29ed2f993fc1b7c254a7a61477e2de544b66b7cb31d00b.png)

Copy your Production API Key to a safe place, you will use it during application setup.

## Install and Launch MongoDB

When a user registers with your application, a request is made to Twilio to add that user to your App, and a `user_id` is returned. In this demo, we'll store the returned `user_id` in a MongoDB database.

Instructions for installing MongoDB vary by platform. Follow the instructions you need to install locally.

* [Install MongoDB for Mac OSX](https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-os-x/)
* [Install MongoDB for Linux](https://www.mongodb.com/docs/manual/administration/install-on-linux/)
* [Install MongoDB for Windows](https://www.mongodb.com/docs/manual/tutorial/install-mongodb-on-windows/)

After installing, launch MongoDB. For \*NIX and OSX, you can run:

```bash
mongod
```

## Setup Authy on Your Device

This two-factor authentication demos two channels which require an installed Authy app to test: Soft tokens and push authentications. While SMS and voice channels will work without the client, to try out all four authentication channels download and install the Authy app for Desktop or Mobile:

* [Download Authy App](https://authy.com/download/)

## Clone and Setup the Application

[Clone our Node.js repository locally](https://github.com/TwilioDevEd/account-security-quickstart-node), then enter the directory. Install all of the necessary node modules:

```bash
npm install
```

Next, open the file `.env.example`. There, **edit** the `ACCOUNT_SECURITY_API_KEY`, pasting in the API Key from the above step (in the console), and **save** the file as `.env`.

Depending on your system, you need to set the environmental variables before you continue. On \*NIX, you can run:

```bash
source .env
```

On Windows, depending on your shell, you will have to use `SET`.

Alternatively, you could use a package such as [autoenv](https://www.npmjs.com/package/autoenv) to load it at startup.

```bash title="Add Your Application API Key" description="Enter the API Key from the Account Security console and optionally change the port."
export ACCOUNT_SECURITY_API_KEY='ENTER_SECRET_HERE'
export PORT=1337
```

Once you have added your API Key, you are ready to run! Launch Node with:

```bash
node .
```

If MongoDB is running and your API Key is correct, you should get a message your new app is running!

## Try the Node.js Two-Factor Demo

With your phone (optionally with the Authy client installed) nearby, open a new browser tab and go to `http://localhost:1337/register`

Enter your information and invent a password, then hit 'Register'. Your information is passed to Twilio (you will be able to see your user [immediately in the console](https://www.twilio.com/console/authy/applications/)), and the application is returned a `user_id`.

Go to `http://localhost:1337/login` and login. You'll be presented with a happy screen:

![Token verification options: SMS, Voice, Push Auth, and Verify button.](https://docs-resources.prod.twilio.com/04aaadf5246b991095eed6ed1a7dbb06a183a169a1a2798bad8982ca0f1c0bb0.png)

If your phone has the Authy app installed, you can immediately enter a soft token from the client to Verify. Additionally, you can try a push authentication by pushing the labeled button.

If you do not have the Authy app installed, the SMS and voice channels will also work in providing a token. To try different channels, you can logout to start the process again.

```js title="Two-Factor Authentication Channels" description="Demonstrating SMS, Voice, and Push Notification Two-Factor channels. (Soft Tokens can be entered directly.)"
// !mark(157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295)
var crypto = require('crypto');
var mongoose = require('mongoose');
var User = mongoose.model('User');
var config = require('../config.js');
var qs = require('qs');
var request = require('request');
var phoneReg = require('../lib/phone_verification')(config.API_KEY);

// https://github.com/seegno/authy-client
const Client = require('authy-client').Client;
const authy = new Client({key: config.API_KEY});


function hashPW(pwd) {
    return crypto.createHash('sha256').update(pwd).digest('base64').toString();
}

/**
 * Login a user
 * @param req
 * @param res
 */
exports.login = function (req, res) {
    User.findOne({username: req.body.username})
        .exec(function (err, user) {
            if (!user) {
                err = 'Username Not Found';
            } else if (('password' in req.body) && (user.hashed_password !==
                hashPW(req.body.password.toString()))) {
                err = 'Wrong Password';
            } else {
                createSession(req, res, user);
            }

            if (err) {
                res.status(500).json(err);
            }
        });
};

/**
 * Logout a user
 *
 * @param req
 * @param res
 */
exports.logout = function (req, res) {
    req.session.destroy(function (err) {
        if (err) {
            console.log("Error Logging Out: ", err);
            return next(err);
        }
        res.status(200).send();
    });
};

/**
 * Checks to see if the user is logged in and redirects appropriately
 *
 * @param req
 * @param res
 */
exports.loggedIn = function (req, res) {
    if (req.session.loggedIn && req.session.authy) {
        res.status(200).json({url: "/protected"});
    } else if (req.session.loggedIn && !req.session.authy) {
        res.status(200).json({url: "/2fa"});
    } else {
        res.status(409).send();
    }
};

/**
 * Sign up a new user.
 *
 * @param req
 * @param res
 */
exports.register = function (req, res) {

    var username = req.body.username;
    User.findOne({username: username}).exec(function (err, user) {
        if (err) {
            console.log('Rregistration Error', err);
            res.status(500).json(err);
            return;
        }
        if (user) {
            res.status(409).json({err: "Username Already Registered"});
            return;
        }

        user = new User({username: req.body.username});

        user.set('hashed_password', hashPW(req.body.password));
        user.set('email', req.body.email);
        user.set('authyId', null);
        user.save(function (err) {
            if (err) {
                console.log('Error Creating User', err);
                res.status(500).json(err);
            } else {

                authy.registerUser({
                    countryCode: req.body.country_code,
                    email: req.body.email,
                    phone: req.body.phone_number
                }, function (err, regRes) {
                    if (err) {
                        console.log('Error Registering User with Authy');
                        res.status(500).json(err);
                        return;
                    }

                    user.set('authyId', regRes.user.id);

                    // Save the AuthyID into the database then request an SMS
                    user.save(function (err) {
                        if (err) {
                            console.log('error saving user in authyId registration ', err);
                            res.session.error = err;
                            res.status(500).json(err);
                        } else {
                            createSession(req, res, user);
                        }
                    });
                });
            }
        });
    });
};


/**
 * Check user login status.  Redirect appropriately.
 *
 * @param req
 * @param res
 */
exports.loggedIn = function (req, res) {

    if (req.session.loggedIn && req.session.authy) {
        res.status(200).json({url: "/protected"});
    } else if (req.session.loggedIn && !req.session.authy) {
        res.status(200).json({url: "/2fa"});
    } else {
        res.status(200).json({url: "/login"});
    }
};

/**
 * Request a OneCode via SMS
 *
 * @param req
 * @param res
 */
exports.sms = function (req, res) {
    var username = req.session.username;
    User.findOne({username: username}).exec(function (err, user) {
        console.log("Send SMS");
        if (err) {
            console.log('SendSMS', err);
            res.status(500).json(err);
            return;
        }

        /**
         * If the user has the Authy app installed, it'll send a text
         * to open the Authy app to the TOTP token for this particular app.
         *
         * Passing force: true forces an SMS send.
         */
        authy.requestSms({authyId: user.authyId}, {force: true}, function (err, smsRes) {
            if (err) {
                console.log('ERROR requestSms', err);
                res.status(500).json(err);
                return;
            }
            console.log("requestSMS response: ", smsRes);
            res.status(200).json(smsRes);
        });

    });
};

/**
 * Request a OneCode via a voice call
 *
 * @param req
 * @param res
 */
exports.voice = function (req, res) {
    var username = req.session.username;
    User.findOne({username: username}).exec(function (err, user) {
        console.log("Send SMS");
        if (err) {
            console.log('ERROR SendSMS', err);
            res.status(500).json(err);
            return;
        }

        /**
         * If the user has the Authy app installed, it'll send a text
         * to open the Authy app to the TOTP token for this particular app.
         *
         * Passing force: true forces a voice call to be made
         */
        authy.requestCall({authyId: user.authyId}, {force: true}, function (err, callRes) {
            if (err) {
                console.error('ERROR requestcall', err);
                res.status(500).json(err);
                return;
            }
            console.log("requestCall response: ", callRes);
            res.status(200).json(callRes);
        });
    });
};

/**
 * Verify an Authy Token
 *
 * @param req
 * @param res
 */
exports.verify = function (req, res) {
    var username = req.session.username;
    User.findOne({username: username}).exec(function (err, user) {
        console.log("Verify Token");
        if (err) {
            console.error('Verify Token User Error: ', err);
            res.status(500).json(err);
        }
        authy.verifyToken({authyId: user.authyId, token: req.body.token}, function (err, tokenRes) {
            if (err) {
                console.log("Verify Token Error: ", err);
                res.status(500).json(err);
                return;
            }
            console.log("Verify Token Response: ", tokenRes);
            if (tokenRes.success) {
                req.session.authy = true;
            }
            res.status(200).json(tokenRes);
        });
    });
};

/**
 * Create a OneTouch request.
 * The front-end client will poll 12 times at a frequency of 5 seconds before terminating.
 * If the status is changed to approved, it quit polling and process the user.
 *
 * @param req
 * @param res
 */
exports.createonetouch = function (req, res) {

    var username = req.session.username;
    console.log("username: ", username);
    User.findOne({username: username}).exec(function (err, user) {
        if (err) {
            console.error("Create OneTouch User Error: ", err);
            res.status(500).json(err);
        }

        var request = {
            authyId: user.authyId,
            details: {
                hidden: {
                    "test": "This is a"
                },
                visible: {
                    "Authy ID": user.authyId,
                    "Username": user.username,
                    "Location": 'San Francisco, CA',
                    "Reason": 'Demo by Authy'
                }
            },
            message: 'Login requested for an Authy Demo account.'
        };

        authy.createApprovalRequest(request, {ttl: 120}, function (oneTouchErr, oneTouchRes) {
            if (oneTouchErr) {
                console.error("Create OneTouch Error: ", oneTouchErr);
                res.status(500).json(oneTouchErr);
                return;
            }
            console.log("OneTouch Response: ", oneTouchRes);
            req.session.uuid = oneTouchRes.approval_request.uuid;
            res.status(200).json(oneTouchRes)
        });

    });
};

/**
 * Verify the OneTouch request callback via HMAC inspection.
 *
 * @url https://en.wikipedia.org/wiki/Hash-based_message_authentication_code
 * @url https://gist.github.com/josh-authy/72952c62521480f3dd710dcbad0d8c42
 *
 * @param req
 * @return {Boolean}
 */
function verifyCallback(req) {

    var apiKey = config.API_KEY;

    var url = req.headers['x-forwarded-proto'] + "://" + req.hostname + req.url;
    var method = req.method;
    var params = req.body;

    // Sort the params.
    var sorted_params = qs.stringify(params).split("&").sort().join("&").replace(/%20/g, '+');

    var nonce = req.headers["x-authy-signature-nonce"];
    var data = nonce + "|" + method + "|" + url + "|" + sorted_params;

    var computed_sig = crypto.createHmac('sha256', apiKey).update(data).digest('base64');
    var sig = req.headers["x-authy-signature"];

    return sig == computed_sig;
}

/**
 * Poll for the OneTouch status.  Return the response to the client.
 * Set the user session 'authy' variable to true if authenticated.
 *
 * @param req
 * @param res
 */
exports.checkonetouchstatus = function (req, res) {

    var options = {
        url: "https://api.authy.com/onetouch/json/approval_requests/" + req.session.uuid,
        form: {
            "api_key": config.API_KEY
        },
        headers: {},
        qs: {
            "api_key": config.API_KEY
        },
        json: true,
        jar: false,
        strictSSL: true
    };

    request.get(options, function (err, response) {
        if (err) {
            console.log("OneTouch Status Request Error: ", err);
            res.status(500).json(err);
        }
        console.log("OneTouch Status Response: ", response);
        if (response.body.approval_request.status === "approved") {
            req.session.authy = true;
        }
        res.status(200).json(response);
    });
};

/**
 * Register a phone
 *
 * @param req
 * @param res
 */
exports.requestPhoneVerification = function (req, res) {
    var phone_number = req.body.phone_number;
    var country_code = req.body.country_code;
    var via = req.body.via;

    console.log("body: ", req.body);

    if (phone_number && country_code && via) {
        phoneReg.requestPhoneVerification(phone_number, country_code, via, function (err, response) {
            if (err) {
                console.log('error creating phone reg request', err);
                res.status(500).json(err);
            } else {
                console.log('Success register phone API call: ', response);
                res.status(200).json(response);
            }
        });
    } else {
        console.log('Failed in Register Phone API Call', req.body);
        res.status(500).json({error: "Missing fields"});
    }

};

/**
 * Confirm a phone registration token
 *
 * @param req
 * @param res
 */
exports.verifyPhoneToken = function (req, res) {
    var country_code = req.body.country_code;
    var phone_number = req.body.phone_number;
    var token = req.body.token;

    if (phone_number && country_code && token) {
        phoneReg.verifyPhoneToken(phone_number, country_code, token, function (err, response) {
            if (err) {
                console.log('error creating phone reg request', err);
                res.status(500).json(err);
            } else {
                console.log('Confirm phone success confirming code: ', response);
                if (response.success) {
                    req.session.ph_verified = true;
                }
                res.status(200).json(err);
            }

        });
    } else {
        console.log('Failed in Confirm Phone request body: ', req.body);
        res.status(500).json({error: "Missing fields"});
    }
};

/**
 * Create the initial user session.
 *
 * @param req
 * @param res
 * @param user
 */
function createSession(req, res, user) {
    req.session.regenerate(function () {
        req.session.loggedIn = true;
        req.session.user = user.id;
        req.session.username = user.username;
        req.session.msg = 'Authenticated as: ' + user.username;
        req.session.authy = false;
        req.session.ph_verified = false;
        res.status(200).json();
    });
}
```

And there you go, two-factor authentication is on and your Node.js app is protected!

## What's Next?

Now that you are keeping the hackers out of this demo app using two-factor authentication, you can find detailed descriptions of options and API calls in the [Authy API Reference](/docs/authy/api). If you're also building a registration flow, check out the [Twilio Verify](/docs/verify) product, which uses this codebase.
