2FA SIM Swap Detection with MessageBird Verify

In this tutorial, you'll learn how to add SIM Swap Detection to your existing Web App's MessageBird two-factor authentication (2FA) SMS login flow using SIMCheck.
Timothy OgbemudiaDeveloper Experience Engineer
Last updated: 23 August 2022
tutorial cover image

The MessageBird Verify API is an SMS OTP 2FA provider. It is used to verify that a user possesses a phone with an associated phone number. However, with SIM swap cases on the rise, it is imperative to do more to secure user applications by also detecting if the user's SIM has changed recently. It could indicate that the user has been the victim of a SIM swap attack. This problem is where the tru.ID SIMCheck API comes in.

The tru.ID SIMCheck API provides information on when the SIM card associated with a mobile phone number was last changed. This check provides an extra layer of security in your application login flows by indicating attempted SIM Swap Fraud. It can be used to augment existing 2FA or anti-fraud workflows. Here, we will use SIMCheck to augment an existing 2FA workflow using MessageBird.

If you'd prefer to dive into the completed code, head to the GitHub repo.

Requirements

The requirements for this project are:

Getting Started

Clone the starter-files branch via:

git clone -b starter-files https://github.com/tru-ID/2fa-sim-swap-detection-messagebird.git

Next, you need to configure MessageBird's credentials. Copy the values of env.example into an .env file:

cd 2fa-sim-swap-detection-messagebird && cp env.example .env

Open the .env file and configure the MESSAGEBIRD_API_KEY value to be the API Key found on the MessageBird dashboard.

MESSAGEBIRD_API_KEY={YOUR_API_KEY}
A tru.ID Account is needed to make the SIMCheck 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
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
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 2fa-with-messagebird --project-dir .

Configure the following values in your .env:

  • TRU_ID_CLIENT: The client ID found in the tru.json file in the newly created tru.ID project.
  • TRU_ID_SECRET: The client secret found in the tru.json file in the newly created tru.ID project.

Install dependencies via:

npm install

Starting project

To start the project, run the following in the terminal:

npm start

The project should look like this:

tru.ID + MessageBird 2FA Starter App

Existing workflow

The existing workflow of the application is as follows:

  1. App opens up in /, where the user inputs their phone number in E.164 International Format and submits the form.
  2. User gets taken to the /step2 route, where the user enters the OTP code they've received via SMS.
  3. If the OTP code is valid, the user gets taken to a /step3 route; otherwise, an error is rendered.

Augmented workflow with SIM swap detection

  1. App opens up in /, where the user inputs their phone number in E.164 International Format and submits the form.
  2. If the SIMCheck is successful, the user gets taken to the /step2 route, where the user enters the OTP code they've received via SMS. If the SIMCheck fails, a dynamic error message with what went wrong is shown to the user.
  3. If the OTP code is valid, the user gets taken to a /step3 route; otherwise, an error is rendered.

Performing the SIMCheck

To perform the SIMCheck, we need to do two things:

  • Create a tru.ID access token
  • Create a SIMCheck using the newly generated access token. To do this, we need to bring in a few packages. Open a new terminal and run:
npm install --save btoa node-fetch

btoa transforms data to base-64 encoded format, and node-fetch allows us to make HTTP network requests in our Node applications.

Creating the Access Token

To create the access token, create a new directory called helpers, and create a file named createAccessToken.js with the following:

const btoa = require('btoa')
const fetch = require('node-fetch')
exports.createAccessToken = async () => {
/* make request body acceptable by application/x-www-form-urlencoded*/
const clientID = process.env.TRU_ID_CLIENT
const clientSecret = process.env.TRU_ID_SECRET
const basicAuth = btoa(`${clientID}:${clientSecret}`)
const resp = await fetch(
`https://{data_residency}.api.tru.id/oauth2/v1/token`,
{
method: 'POST',
body: 'grant_type=client_credentials&scope=sim_check',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${basicAuth}`,
},
},
)
const { access_token } = await resp.json()
return access_token
}

To create an access token, we make a form URL encoded POST request to the https://{data_residency}.api.tru.id/oauth2/v1/token endpoint. This endpoint uses basic auth, requiring an Authorization header.

The header value is your tru.ID project client_id and client_secret, which we saved to the .env file earlier, concatenated with a colon (:) and Base64 encoded.

The body is set to have a grant_type of client_credentials and scope of sim_check.

The scope instructs the tru.ID OAuth2 provider that the created Access Token should have permissions to use SIMCheck resources, as indicated by sim_check.

Creating the SIMCheck

In the helpers directory, create a file named performSimCheck.js and paste the following code:

const fetch = require('node-fetch')
exports.performSimCheck = async (phoneNumber, accessToken) => {
let simChanged
let numberSupported = true
const body = JSON.stringify({ phone_number: phoneNumber })
const response = await fetch(
`https://{data_residency}.api.tru.id/sim_check/v0.1/checks`,
{
method: 'POST',
body,
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
},
)
if (response.status === 201) {
const data = await response.json()
console.log(data)
simChanged = !data.no_sim_change
} else if (response.status === 400) {
numberSupported = false
} else {
throw new Error(`Unexpected API response ${res.status}`, res.toString())
}
return { simChanged, numberSupported }
}

Here, we accept a phone_number in E.164 format, the access_token from the previous step, and create the SIMCheck resource.

A successful SIMCheck resource creation response is represented by a 201 HTTP status code. The payload response provides information on whether the SIM has changed or not, and we have two variables: simChanged and numberSupported, that we set based on the API response.

A successful sample response is:

{
"_links": {
"self": {
"href": "https://{data_residency}.api.tru.id/sim_check/v0.1/c6-bbae-50358ae3bb08"
}
},
"check_id": "2563f2a6-0b9e-49c6-bbae-50358ae3bb08",
"status": "COMPLETED",
"no_sim_change": true,
"charge_amount": 1,
"charge_currency": "API",
"created_at": "2021-05-04T14:52:29+0000",
"snapshot_balance": 50
}

A 400 HTTP status may be returned if tru.ID doesn't support the phone number. An example of that is as follows:

{
type: 'https://tru.id/docs/api-errors#mno_not_supported',
title: 'Bad Request',
status: 400,
detail: '400 Bad Request Mobile Network Operator Not Supported'
}

Handling the failure case

As the next step, we need to handle the following scenarios in our application:

  • The SIM changed recently, indicated by simChanged being true.
  • tru.ID cannot perform a lookup on the phone number, resulting in numberSupported being false.

To do this, head over to the views directory, create a file named: error.handlebars, and paste the following code:

<p>Cannot proceed 😢 {{error}}</p>

Here we tell the user they cannot proceed and pass in a dynamic error message.

The view for a 400 will look like this:

tru.ID + MessageBird 2FA Error View

Integrating our helper functions

Next, we need to integrate our helper functions.

First, add the imports in index.js:

const { createAccessToken } = require('./helpers/createAccessToken')
const { performSimCheck } = require('./helpers/performSimCheck')

Make the /step2 route handler function asynchronous by appending the async keyword to the front.

app.post('/step2', async function(req, res) {

Next, within the asynchronous callback, paste the following after var number = req.body.number; and before messagebird.verify.create:

//create an access token
const accessToken = await createAccessToken()
// perform SIMCheck
const { simChanged, numberSupported } = await performSimCheck(
number,
accessToken,
)
if (simChanged === true) {
return res.render('error', {
error:
'Verification Failed. SIM changed too recently. Please contact support.',
})
}
if (numberSupported === false) {
return res.render('error', {
error:
'Verification Failed. We do not support the phone number. Please contact support.',
})
}

Here we do the following:

  • Create a tru.ID access token via our helper method
  • Perform a SIMCheck passing the access token and the phone number
  • If simChanged equals true, the SIM changed recently, and we render our error view passing in a dynamic error message
  • If numberSupported is false, return our dynamic error view, passing in the error message that indicates the phone number is not supported
  • If the SIM hasn't changed recently, we continue with MessageBird to create the OTP

If the SIMCheck fails, the user will be presented with the following:

tru.ID + MessageBird SIM Change Detected

The views for a successful scenario look like this:

tru.ID + MessageBird 2FA Starter View

tru.ID + MessageBird 2FA Verify PIN code View

tru.ID + Vonage 2FA Phone Number Verified View

Wrapping up

That's it! That's how simple it is to add SIM Swap detection to your existing MessageBird 2FA application with tru.ID's SIMCheck API.

You can view the difference between the Vonage base and the finished app here.

Resources

Download our Developer Console mobile app
Made withacross the 🌍
© 2024 4Auth Limited. All rights reserved. tru.ID is the trading name of 4Auth Limited.