DEV Community

Cover image for Web-Based Secure Password Reset with tru.ID and Vonage
Greg Holmes for tru.ID

Posted on • Originally published at developer.tru.id

Web-Based Secure Password Reset with tru.ID and Vonage

The tru.ID SubscriberCheck API confirms the ownership of a mobile phone number by confirming the presence of an active SIM card with the same number. A mobile data session is created with a request to a Check URL unique to this SIM card.

tru.ID then resolves a match between the phone number entered by the user and the phone number that the mobile network operator (MNO) identifies as the owner of the mobile data session.

Any website or mobile application that requires some form of authentication using an email or username and a password will have a password reset flow. Usually, this flow involves the user providing identifying information (email, username, phone number, etc.) and then receiving a code via email or SMS or being asked to answer a secret question.

This is how phishing attackers often gain fraudulent access to online accounts by intercepting the code or secret answer. With tru.ID, this exploitation is no longer possible.

In this tutorial, you will start with a basic web application that doesn't yet provide any form of password reset flow.

As you progress through the tutorial, you will adapt your codebase first to check whether the phone number associated with a user’s account is a phone number from a mobile network operator supported by tru.ID. If the phone number is supported, then the tru.ID SubscriberCheck flow will begin – firstly, to check that the phone number and the data connection match, and secondly, to check the SIM card hasn't recently swapped.

By the end of this tutorial, you will have successfully implemented a password reset flow with tru.ID's SubscriberCheck as the primary method of multi-factor authentication, and Vonage's Verify API as the secondary. If the SubscriberCheck is the triggered flow, the user will have a seamless verification process with no action required and no attack vector.

Before you begin

To follow along with this tutorial, you'll need the following:

  • A tru.ID Account
  • Node.js installed locally on your computer
  • A mobile phone with an active data SIM card

Getting started

A tru.ID Account is needed to make the SubscriberCheck API requests, so make sure you've created one.

You're also going to need some Project credentials from tru.ID to make API calls. So sign up for a tru.ID account, which comes with some free credit. We've built a CLI for you to manage your tru.ID account, projects, and credentials within your Terminal. To install the tru.ID CLI run the following command:

npm install -g @tru_id/cli
Enter fullscreen mode Exit fullscreen mode

Run tru login <YOUR_IDENTITY_PROVIDER> (this is one of google, github, or microsoft) using the Identity Provider you used when signing up. This command will open a new browser window and ask you to confirm your login. A successful login will show something similar to the below:

Success. Tokens were written to /Users/user/.config/@tru_id/cli/config.json. You can now close the browser
Enter fullscreen mode Exit fullscreen mode

Note: The config.json file contains some information you won't need to modify. This config file includes your Workspace Data Residency (EU, IN, US), your Workspace ID, and token information such as your scope.

Create a new tru.ID project within the root directory with the following command:

tru projects:create password-reset-web --project-dir .
Enter fullscreen mode Exit fullscreen mode

Make a note of these tru.ID credentials because you'll need them later in the tutorial.

Note: The users set up in the database for test purposes are configured based on tru.ID's SubscriberCheck Sandbox conditions.

In your Terminal, clone the starter-files branch with the following command:

git clone -b starter-files git@github.com:tru-ID/web-based-password-reset-tutorial.git
Enter fullscreen mode Exit fullscreen mode

If you're only interested in the finished code in main, then run:

git clone -b main git@github.com:tru-ID/web-based-password-reset-tutorial.git
Enter fullscreen mode Exit fullscreen mode

The starter-files branch already contains some third-party libraries needed to get you started. So in your Terminal, within the project directory, run the following command to install these third-party libraries:

npm install
npx sequelize-cli db:migrate # to create the database, and run the migrations.
npx sequelize-cli db:seed:all # to seed the test users into the database.
Enter fullscreen mode Exit fullscreen mode

First, you'll need to expose your project to the Internet for a callback. So to initialize the ngrok tunnel by running the following command:

npx ngrok http 3000
Enter fullscreen mode Exit fullscreen mode

Next, to run the web server, run the following command in your Terminal, in the project directory:

npm start
Enter fullscreen mode Exit fullscreen mode

Once you've started the server, you'll have also created a ngrok tunnel, which shows output similar to what's shown in the example below:

[nodemon] 2.0.16
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node index.js`
Example app listening at http://localhost:4000
Starting Ngrok now
Ngrok connected, URL: https://<subdomain>.ngrok.io
Enter fullscreen mode Exit fullscreen mode

Below is a screenshot of the landing page you'll be shown when you open localhost:4000 or your ngrok URL in your browser:

A screenshot of the demo application's landing page. A white background, with a grey bar at the top, labelled 'My Web App'. Below this, side by side, are two blue buttons, one labelled 'Login', the other labelled 'Register'

The Login screen reached by clicking the Login button will show you a page similar to what's shown in the image below:

A screenshot of the demo application's landing page. A white background, with a grey bar, labelled 'My Web App' at the top. Below is a form containing two fields, 'Email' and 'Password' with their appropriate labels, followed by a Blue button labelled 'Login' and a link 'I forgot my password.

Implementing tru.ID SubscriberCheck

Install tru.ID Web SDK

To carry out a SubscriberCheck, we'll be using the tru.ID Web SDK. Install this SDK into your application with the following command:

npm install @tru_id/tru-sdk-web
Enter fullscreen mode Exit fullscreen mode

tru.ID Access Token

You'll want to avoid hardcoding credentials and sensitive information in your codebase; for this, we'll be using Environment Variables. Install dotenv package into your project:

npm install dotenv
Enter fullscreen mode Exit fullscreen mode

Inside src/server.js find the line: const port = 4000; and below this add the following:

require('dotenv').config()
Enter fullscreen mode Exit fullscreen mode

Now, create a .env file in the root directory of your project and add the following:

TRU_ID_CLIENT_ID=
TRU_ID_CLIENT_SECRET=
EXTERNAL_URL=
Enter fullscreen mode Exit fullscreen mode

Note: Populate the values of the above with your own tru.ID client_id and client_secret, while the EXTERNAL_URL is your ngrok URL.

To make API requests to tru.ID you will need to generate an access token with your client_id and client_secret. First, create a new directory called api within your src directory. And within this new directory, create the tru.js file. Populate this file with the following:

const moment = require("moment");
const fetch = require("node-fetch");
const httpSignature = require("http-signature");
const jwksClient = require("jwks-rsa");

const tru_api_base_url = 'https://eu.api.tru.id';
const keyClient = jwksClient({
  jwksUri: `${tru_api_base_url}/.well-known/jwks.json`,
});

// token cache in memory
const TOKEN = {
  accessToken: undefined,
  expiresAt: undefined,
};

async function getAccessToken() {
  // check if existing valid token
  if (TOKEN.accessToken !== undefined && TOKEN.expiresAt !== undefined) {
    // we already have an access token. Let's check if it's not expired
    // by removing 1 minute because the access token is about to expire, so it's better to refresh anyway
    if (
      moment()
        .add(1, "minute")
        .isBefore(moment(new Date(TOKEN.expiresAt)))
    ) {
      // token not expired
      return TOKEN.accessToken;
    }
  }

  const url = `${tru_api_base_url}/oauth2/v1/token`;

  const toEncode = `${process.env.TRU_ID_CLIENT_ID}:${process.env.TRU_ID_CLIENT_SECRET}`;
  const auth = Buffer.from(toEncode).toString('base64');

  const requestHeaders = {
    Authorization: `Basic ${auth}`,
    "Content-Type": "application/x-www-form-urlencoded",
  };

  const res = await fetch(url, {
    method: "post",
    headers: requestHeaders,
    body: new URLSearchParams({
      grant_type: "client_credentials",
      scope: "phone_check coverage subscriber_check",
    }),
  });

  if (!res.ok) {
    return res.status(400).body("Unable to create access token")
  }

  const json = await res.json();

  // update token cache in memory
  TOKEN.accessToken = json.access_token;
  TOKEN.expiresAt = moment().add(json.expires_in, "seconds").toString();

  return json.access_token;
}
Enter fullscreen mode Exit fullscreen mode

Reachability

Creating the Coverage Check

The first step is to determine whether the data connection from the device is a cellular data connection and is supported by tru.ID – a Coverage check. Within your tru.js file, add the following new method:

async function checkCoverage(req, res) {
  const accessToken = await getAccessToken()
  const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;

  const deviceCoverageResponse = await fetch(
    `${tru_api_base_url}/coverage/v0.1/device_ips/${ip}`,
    {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    },
  )

  if (deviceCoverageResponse.status === 200) {
    // Loop through products to double check we cover SubscriberCheck for this MNO
    const data = await deviceCoverageResponse.json()

    for (var counter = 0, length = data.products.length; counter < length; counter++) {
      if (data.products[counter] !== null) {
        if (data.products[counter].product_id === 'SUK') {
          return true
        }
      }
    }
  } else if (deviceCoverageResponse.status === 400) {
    // tru.ID has no coverage for this mobile network operator default to SMS
    console.log('tru.ID has no coverage for this mobile network operator. Sending an SMS OTP')
    return false
  } else if (deviceCoverageResponse.status === 412) {
    // Default to SMS
    console.log('IP address is not a mobile IP Address, here, you could suggest the user disabled WiFi. Sending an SMS OTP')
    return false
  } else {
    // Unexpected result from device coverage check. Default to SMS
    console.log('Unexpected result from coverage check. Sending an SMS OTP')
    return false
  }
}

module.exports = {
  getAccessToken,
  checkCoverage
}
Enter fullscreen mode Exit fullscreen mode

Create a new routes file within /src/routes called tru.js. This file will contain the API routes your server needs to process the tru.ID SubscriberCheck flow.

Within this new file, add the following functionality. This functionality takes the IP address of the device accessing the web page to determine whether it is on a mobile network IP address or tru.ID and the mobile network operator has coverage.

const truApi = require('../api/tru')

async function checkCoverage(req, res) {
  try {
    const coverageCheckRes = await truApi.checkCoverage(req, res)

    return res.status(200).json({coverage: coverageCheckRes})
  } catch (error) {
    console.log(error)
    res.sendStatus(500)
  }
}

module.exports = {
  checkCoverage,
}
Enter fullscreen mode Exit fullscreen mode

Now you need to expose this new endpoint through the server. So, open /src/routes.js, and at the top, add the import for your tru.js routes file:

const tru = require('./routes/tru')
Enter fullscreen mode Exit fullscreen mode

Within your routes() function, add the following definition for this /api/coverage endpoint, which will be called from the user's browser:

  router.get('/api/coverage', tru.checkCoverage)
Enter fullscreen mode Exit fullscreen mode

Calling the Coverage Check

To call this coverage check, you need to update the password-reset endpoint's template. Open src/views/password-reset.hbs, and at the bottom of the file, add the following:

<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tru_id/tru-sdk-web/dist/tru-id-sdk.umd.js"></script>

<script>
  // Step 1 - Do coverage check
  async function checkCoverage(ev) {
    try {
      const deviceCoverageResult = await axios.get('/api/coverage', {
        validateStatus: (status) => status >= 200 && status <= 412,
      })

      // If there's no coverage, prompt the user to turn off WiFi if it's enabled and recheck.
      if (deviceCoverageResult.status === 200 && deviceCoverageResult.data.coverage === true) {
        // tru.ID has coverage.
        await createSubscriberCheck(ev)
      } else if (deviceCoverageResult.status === 400 || deviceCoverageResult.status === 412) {
        // No coverage continue with POST request
        console.log('no coverage')
        return true
      } else {
        // Unexpected result, continue with POST request
        console.log('unexpected result')
        return true
      }
    } catch (ex) {
      // Unexpected result, continue with POST request
      console.log('error')
      console.log(ex)
      return true
    }
  }

  // Step 2 - If covered, do SubscriberCheck(based on the email address....)
  async function createSubscriberCheck(ev) {

  }

  document
    .getElementById('password_reset')
    .addEventListener('submit', async (event) => {
      const coverage = await checkCoverage()
      if (coverage !== true) {
        event.preventDefault()
      }
    })
</script>
Enter fullscreen mode Exit fullscreen mode

The above code imports axios (A promise-based HTTP client for the browser) and tru.ID's SDK. Following the import, the code creates a function called checkCoverage.

Within this function, a GET request is made to your /api/coverage endpoint to check whether the device is supported based on its IP address. At the bottom of this code, you'll see an event listener. This listener can only call the checkCoverage function when the user has submitted the button.

SubscriberCheck

Creating the SubscriberCheck

The SubscriberCheck flow is as follows:

  • 1 - Your Backend makes a POST request to tru.ID to create a check.
  • 2 - tru.ID returns in the response a check_url.
  • 3 - Your web application in the user's browser opens that check_url in the background.
  • 4 - This check_url is direct to that user's mobile network operator (MNO). The MNO verifies the request.
  • 5 - Your backend makes a PATCH request to tru.ID with the code returned at the redirect_url defined when creating the check.

So, open your /src/api/tru.js file and add the following code for this functionality from your Backend's actions:

async function createSubscriberCheck(phone) {
  // Create SubscriberCheck resource
  const token = await getAccessToken()

  const subscriberCheckCreateResult = await fetch(`${tru_api_base_url}/subscriber_check/v0.2/checks`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      phone_number: phone,
      redirect_url: `${process.env.EXTERNAL_URL}/api/sck-password-reset-code`,
    }),
  });

  if (subscriberCheckCreateResult.status === 201) {
    const data = await subscriberCheckCreateResult.json()

    return data
  }

  return false
}

async function patchSubscriberCheck(check_id, code) {
  const url = `${tru_api_base_url}/subscriber_check/v0.2/checks/${check_id}`
  const body = [{ op: 'add', path: '/code', value: code }]

  const token = await getAccessToken()
  const requestHeaders = {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json-patch+json',
  }

  const subscriberCheckPatchResult = await fetch(url, {
    method: 'PATCH',
    headers: requestHeaders,
    body: JSON.stringify(body),
  });

  if (subscriberCheckPatchResult.status === 200) {
    const data = await subscriberCheckPatchResult.json()

    return data
  }

  return {}
}

async function getSubscriberCheckStatus(checkId) {
  const url = `${tru_api_base_url}/subscriber_check/v0.2/checks/${checkId}`

  const token = await getAccessToken()
  const requestHeaders = {
    Authorization: `Bearer ${token}`,
    'Content-Type': 'application/json',
  }

  const subscriberCheckResult = await fetch(url, {
    method: 'GET',
    headers: requestHeaders,
  });

  if (subscriberCheckResult.status === 200) {
    const data = await subscriberCheckResult.json()

    return data
  }

  return {}
}
Enter fullscreen mode Exit fullscreen mode

Now update the exports:

module.exports = {
  getAccessToken,
  checkCoverage,
  createSubscriberCheck,
  patchSubscriberCheck,
  getSubscriberCheckStatus
}
Enter fullscreen mode Exit fullscreen mode

You may have noticed that you've added three functions. All of these will be used through the next steps of this tutorial.

Open /src/routes/tru.js; at the top, add the following two imports:

const db = require('../models')
const { getHashedPassword } = require('../utils/auth')
Enter fullscreen mode Exit fullscreen mode

As previously explained, this file will handle all routes relevant to tru.ID and the SubscriberCheck. So within this file, as well as the checkCoverage function, you'll also have:

  • createSubscriberCheck - This endpoint takes the phone number and redirect_url, making a POST request to tru.ID, requesting a check_url to be returned to the device.
  • SubscriberCheckCallback - The endpoint for the previously defined redirect_url, where the web application will end up following the check_url. This request will contain the request_id and code, which will be sent as a PATCH request to tru.ID to complete the flow.
  • getScPasswordResetCode - Checks the user has an active SubscriberCheck in progress, then renders the template form for the user to enter their email address and their new password.
  • postScPasswordResetCode - Checks the user has an active SubscriberCheck in progress, then saves the user's new password.

Within the tru.js file, add the following functions:

async function createSubscriberCheck(req, res) {
  const { email } = req.body

  if (!email) {
    return res
      .status(400)
      .json({ error_message: 'email parameter is required' })
  }

  const user = await db.User.findOne({ where: { email: email } })

  if (!user) {
    return res.redirect('password-reset')
  }

  try {
    const subscriberCheckRes = await truApi.createSubscriberCheck(
      user.phone
    )

    if (subscriberCheckRes === false) {
      return res.sendStatus(400)
    }

    // Select data to send to client
    return res.status(200).json({
      check_id: subscriberCheckRes.check_id,
      check_url: subscriberCheckRes._links.check_url.href
    })
  } catch (error) {
    console.log(error)
    return res.sendStatus(500)
  }
}

async function subscriberCheckCallback(req, res) {
  const subscriberCheckResult = await truApi.patchSubscriberCheck(req.query.check_id, req.query.code)

  if (subscriberCheckResult === {}) {
    return res.redirect('/password-reset-code')
  } else if (subscriberCheckResult.match === false || subscriberCheckResult.no_sim_change === false) {
    // Not a match or sim changed, return to reset password with a message.
    var message = 'Unable to verify, please contact support.'
    var messageClass = 'alert-danger'

    return res.redirect(`/login?message=${message}&messageClass=${messageClass}`)
  } else {
    // Success! Move the user to the reset password form.
    return res.redirect(`/sc-password-reset-code?check_id=${req.query.check_id}`)
  }
}

async function getScPasswordResetCode(req, res) {
  const { check_id } = req.query

  if (!check_id) {
    return res.redirect('/login')
  }

  const subscriberCheckStatus = await truApi.getSubscriberCheckStatus(check_id)

  if (subscriberCheckStatus === {} || subscriberCheckStatus.match === false || subscriberCheckStatus.no_sim_change === false) {
    return res.redirect('/login')
  }

  res.render('sc-password-reset-code', { check_id: check_id })
}

async function postScPasswordResetCode(req, res) {
  const { check_id, email, password, password2 } = req.body

  if (!check_id) {
    return res.redirect('/login')
  }

  const subscriberCheckStatus = await truApi.getSubscriberCheckStatus(check_id)

  if (subscriberCheckStatus === {} || subscriberCheckStatus.match === false || subscriberCheckStatus.no_sim_change === false) {
    return res.redirect('/login')
  }

  const user = await db.User.findOne({ where: { email: email } })

  if (!user) {
     return res.render('login', {
      message: 'Invalid email',
      messageClass: 'alert-danger'
    })
  }

  if (password !== password2) {
    return res.render('login', {
      message: 'Passwords don\'t match',
      messageClass: 'alert-danger'
    })
  }

  const hashedPassword = await getHashedPassword(password)
  await user.update({ password: hashedPassword })
  await user.save()

  var message = 'Password has been reset, please log in.'
  res.redirect(`/login?message=${message}&messageClass=alert-success`)
}
Enter fullscreen mode Exit fullscreen mode

Now update the exports at the bottom of the file:

module.exports = {
  createSubscriberCheck,
  checkCoverage,
  subscriberCheckCallback,
  getScPasswordResetCode,
  postScPasswordResetCode
}
Enter fullscreen mode Exit fullscreen mode

Now create the routes for these endpoints to be accessed. Open /src/routes.js and locate the line router.get('/api/coverage', tru.checkCoverage).

Below this line, add the following four new routes:

  router.post('/api/subscriber-check', tru.createSubscriberCheck)
  router.use('/api/sck-password-reset-code', tru.subscriberCheckCallback)

  router.get('/sc-password-reset-code', tru.getScPasswordResetCode)
  router.post('/sc-password-reset-code', tru.postScPasswordResetCode)
Enter fullscreen mode Exit fullscreen mode

You may have noticed a template called sc-password-reset-code.hbs in the new code above. You'll need this, so in /src/views create a new file called sc-password-reset-code.hbs and add the following:

<div class="row justify-content-md-center" style="margin-top: 100px">
    <div class="col-md-6">

        {{#if message}}
            <div class="alert {{messageClass}}" role="alert">
                {{message}}
            </div>
        {{/if}}

        <form method="POST" action="/sc-password-reset-code">
            <input name="check_id" type="hidden" value="{{check_id}}">
            <div class="form-group">
                <label for="emailInput">Email address</label>
                <input name="email" type="email" class="form-control" id="emailInput" placeholder="Enter email">
            </div>
            <div class="form-group">
                <label for="passwordInput">New Password</label>
                <input name="password" type="password" class="form-control" id="passwordInput" placeholder="New Password">
            </div>
            <div class="form-group">
                <label for="passwordInput2">Re-enter your new Password</label>
                <input name="password2" type="password" class="form-control" id="passwordInput2" placeholder="Re-enter New Password">
            </div>
            <button type="submit" class="btn btn-primary">Reset my Password</button>
        </form>
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Now, back within /src/views/password-reset.hbs, you'll need the functionality to call these new routes and complete the SubscriberCheck section of the flow. Find the Step 2 Line for the function async function createSubscriberCheck(ev) {. Within this function, add the following:

    console.log('creating subscriber check')
    try {
      const emailInput = document.getElementById('emailInput')

      let emailValue = emailInput.value
      // strip spaces out of the phone number and replace them within an input
      emailValue = emailValue.replace(/\s+/g, '')
      emailInput.value = emailValue
      emailInput.blur()

      // Create SubscriberCheck resource
      const subscriberCheckCreateResult = await axios.post('/api/subscriber-check', {
        email: emailValue
      })

      if (subscriberCheckCreateResult.status === 200) {
        // Step 3 - Open Check URL. If successful, end up on `password-reset-code`. If it fails, call the fallback endpoint. (SMS)
        console.log('opening check url')
        console.log(subscriberCheckCreateResult)
        await tru.ID.openCheckUrl(subscriberCheckCreateResult.data.check_url, {
          checkMethod: 'image',
          debug: true,
          // we don't care here as we are already doing
          // the device coverage check automatically on page load
          // through the node server
          checkDeviceCoverage: false,
        })
      } else {
        return true
      }
    } catch (error) {
      console.log(error)

      if (error.response.status === 400) {
        console.log('Your Mobile Network is not supported or you may be on WiFi, if so disconnect from WiFi.')
      } else {
        console.log('An error occurred while creating a SubscriberCheck.')
      }
      return true
    }
Enter fullscreen mode Exit fullscreen mode

The above piece of code first creates the SubscriberCheck; then, if you've successfully created a check, it will open the check_url returned from the API request and complete the flow. Otherwise, the user will be redirected to the Vonage Verify flow.

Vonage Verification

If the device's IP address is not a mobile network or the network provided is not supported by tru.ID, a fallback is required. We'll use Vonage's Verification flow for this tutorial to send the verification code via SMS. However, you can define the workflow in Vonage's documentation.

To start implementing Vonage's Verify API into the flow, in your Terminal, in the project's root directory, run the following command:

npm install @vonage/server-sdk
Enter fullscreen mode Exit fullscreen mode

Open your .env file and update it with the following extra variables. These variables are of your Vonage API credentials:

VONAGE_API_KEY=
VONAGE_API_SECRET=
BRAND_NAME=
WORKFLOW_ID=6 # SMS only.
Enter fullscreen mode Exit fullscreen mode

Next, create a new file within the api directory to handle your Vonage API calls. Create api/vonage.js and populate this file with the following:

const { Vonage } = require('@vonage/server-sdk')

async function createVerification(phoneNumber) {
  const vonage = new Vonage({
    apiKey: process.env.VONAGE_API_KEY,
    apiSecret: process.env.VONAGE_API_SECRET
  })

  const resp = await vonage.verify.start({
    number: phoneNumber,
    brand: process.env.BRAND_NAME,
    workflow_id: process.env.WORKFLOW_ID
  })  

  if (resp.status === '0') {
    // Success
    return resp.request_id
  }

  return null
}

async function verify(request_id, code) {
  const vonage = new Vonage({
    apiKey: process.env.VONAGE_API_KEY,
    apiSecret: process.env.VONAGE_API_SECRET
  })

  console.log(request_id, code)
  const verify = await vonage.verify.check(request_id, code)

  if (verify.status === "0") {
    // Success
    return verify.request_id

  }

  return null
}

module.exports = {
  createVerification,
  verify
}
Enter fullscreen mode Exit fullscreen mode

This new file you've just created has two functions. The first is to initialise the Verify flow; the second is to handle the response from the user once they receive the code and enter it into the application.

For the Verify flow to work, you'll need to associate a user with a Verify request_id. So first, open /src/models/user.js and, within User.init add the following new field:

requestId: DataTypes.STRING
Enter fullscreen mode Exit fullscreen mode

Now in your Terminal, within the project's root directory, run the following command:

npx sequelize-cli migration:create --name modify_users_add_new_fields
Enter fullscreen mode Exit fullscreen mode

Next, find the new file in /src/migrations. It will be named <timestamp>-modify_users_add_new_field.js.

Within the async up() of this file, replace the contents with:

    return Promise.all([
      queryInterface.addColumn(
        'Users', // table name
        'requestId', // new field name
        {
          type: Sequelize.STRING,
          allowNull: true,
        },
      ),
    ]);
Enter fullscreen mode Exit fullscreen mode

And within the async down() replace the contents with:

    return Promise.all([
      queryInterface.removeColumn('Users', 'requestId'),
    ]);
Enter fullscreen mode Exit fullscreen mode

In your Terminal, run the following command to run the migration:

npx sequelize-cli db:migrate
Enter fullscreen mode Exit fullscreen mode

Locate the /src/routes/auth.js file and add the following import at the top of the file:

const vonage = require('../api/vonage')
Enter fullscreen mode Exit fullscreen mode

Next, within this file, find the function postPasswordReset and replace the last four lines of code:

  res.render('login', {
    message: 'Reset Password Link sent via Email',
    messageClass: 'alert-danger'
  })
Enter fullscreen mode Exit fullscreen mode

With the following:

  const vonageRes = await vonage.createVerification(user.phone)

  if (vonageRes !== null) {
    await user.update({ requestId: vonageRes })
    await user.save()

    return res.redirect('password-reset-code')
  }

  return res.redirect('login')
Enter fullscreen mode Exit fullscreen mode

You'll now need to handle the input from the user, which will contain their email, the verification code, and two copies of their password. Find postPasswordResetCode() and populate this function with the following:

  const { email, password, password2, code } = req.body

  // Check if the user with the same email is also registered
  const user = await db.User.findOne({ where: { email: email } })

  if (!user) {
    res.redirect('/login?message=Invalid email&messageClass=alert-danger')
  }

  if (password !== password2) {
    res.render('register', {
      message: 'Passwords do not match.',
      messageClass: 'alert-danger'
    })
  }

  const vonageRes = await vonage.verify(user.requestId, code)

  if (vonageRes !== null) {
    const hashedPassword = getHashedPassword(password)

    await user.update({ requestId: null, password: hashedPassword })
    await user.save()

    let message = 'Password successfully reset. Please log in with your new credentials.'
    let messageClass = 'alert-success'

    return res.redirect(`/login?message=${message}&messageClass=${messageClass}`)
  }

  let message = 'No password-reset in progress. Please restart.'
  let messageClass = 'alert-danger'

  return res.redirect(`/login?message=${message}&messageClass=${messageClass}`)
Enter fullscreen mode Exit fullscreen mode

Wrapping up

That's it! You've now introduced a secure password reset flow within your web application using tru.ID's SubscriberCheck API, and a Vonage Verify backup in case the SubscriberCheck is not possible.

The SubscriberCheck verifies that the user is the owner of that phone number, SIM card, and active data connection, and that the SIM card has not recently been swapped.

Resources

Top comments (0)