tru.ID logo
LoginSignup

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 Ogbemudia

Developer Experience Engineer

Last updated: 17 May 2021

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. If it has, it could indicate that the user has been the victim of a SIM swap attack. This 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 provides an extra layer of security in your application login flows by providing an indication of attempted SIM Swap Fraud. It can be used to augment existing 2FA or anti-fraud workflows. Here, we will be using SIMCheck to augment an existing 2FA workflow using MessageBird.

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

Requirements

The requirements for this project are:

Getting set up with MessageBird

Clone the starter-files branch via:

$ git clone -b starter-files --single-branch 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}

Getting set up with tru.ID

Create a tru.ID Account and install the tru.ID CLI:

$ npm i -g @tru_id/cli

Set up the CLI with the tru.ID credentials which can be found within the tru.ID console.

$ tru setup:credentials {YOUR_CLIENT_ID} {YOUR_CLIENT_SECRET} EU

Then, create a new tru.ID project:

$ tru projects:create 2fa-with-messagebird

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 /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

In order 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 In order 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://eu.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://eu.api.tru.id/oauth2/v1/token endpoint. This endpoint uses basic auth so requires 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://eu.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 sample successful response is:

{
"_links": {
"self": {
"href": "https://eu.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 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

tru.ID logo

Platform

Docs

DON'T MISS A BEAT — STAY ON THE DOT!

Keep current with industry news and updates from tru.ID.

Follow us on:

Made with ❤️ across the 🌍

© 2021 4Auth Limited. All rights reserved. tru.ID is the trading name of 4Auth Limited.