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 IDlayr SIMCheck API comes in.
The IDlayr 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}
npm install -g @tru_id/cli
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
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 IDlayr 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 thetru.json
file in the newly created IDlayr project.TRU_ID_SECRET
: The client secret found in thetru.json
file in the newly created IDlayr 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:
Existing workflow
The existing workflow of the application is as follows:
- App opens up in
/
, where the user inputs their phone number in E.164 International Format and submits the form. - User gets taken to the
/step2
route, where the user enters the OTP code they've received via SMS. - 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
- App opens up in
/
, where the user inputs their phone number in E.164 International Format and submits the form. - 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. - 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 IDlayr 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_CLIENTconst clientSecret = process.env.TRU_ID_SECRETconst basicAuth = btoa(`${clientID}:${clientSecret}`)const resp = await fetch(`https://{data_residency}.api.idlayr.com/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.idlayr.com/oauth2/v1/token
endpoint.
This endpoint uses basic auth, requiring an Authorization
header.
The header value is your IDlayr 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 IDlayr 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 simChangedlet numberSupported = trueconst body = JSON.stringify({ phone_number: phoneNumber })const response = await fetch(`https://{data_residency}.api.idlayr.com/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.idlayr.com/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 IDlayr doesn't support the phone number. An example of that is as follows:
{type: 'https://idlayr.com/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
beingtrue
. - IDlayr cannot perform a lookup on the phone number, resulting in
numberSupported
beingfalse
.
To do this, head over to the views
directory, create a file named: error.handlebars
, and paste the following code:
<p>Cannot proceed 😢 </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:
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 tokenconst accessToken = await createAccessToken()// perform SIMCheckconst { 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 IDlayr access token via our helper method
- Perform a SIMCheck passing the access token and the phone number
- If
simChanged
equalstrue
, the SIM changed recently, and we render our error view passing in a dynamic error message - If
numberSupported
isfalse
, 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:
The views for a successful scenario look like this:
Wrapping up
That's it! That's how simple it is to add SIM Swap detection to your existing MessageBird 2FA application with IDlayr's SIMCheck API.
You can view the difference between the Vonage base and the finished app here.