SIM Swap Detection with Auth0, React Native, and tru.ID

In this tutorial, you'll learn how to add SIM swap detection to a React Native application authenticated with Auth0 passwordless auth. To detect potential SIM swap attacks, you'll use SIMCheck to verify if the SIM card associated with a phone number has changed recently.
Timothy OgbemudiaDeveloper Experience Engineer
Last updated: 18 August 2022
tutorial cover image

In this tutorial, you'll learn to add SIM swap detection using tru.ID SIMCheck to a React Native application authenticated with Auth0's passwordless login.

Auth0 is an identity provider (IDP) that provides authentication and authorization strategies supporting out-of-the-box solutions for social, passwordless, and credential logins.

Twilio is a communications platform as a service (CPaaS), which is the default SMS one-time password (OTP) service used by Auth0.

The tru.ID SIMCheck API provides information on whether a SIM card associated with a mobile phone number has changed in the last seven days. This check provides an extra layer of security in your application login flows and can be used to detect attempted SIM swap fraud. It can be used to augment existing two-factor authentication (2FA) or anti-fraud workflows.

If you want to dive into the finished code, you can find it on GitHub.

Requirements

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

Getting started

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

git clone -b starter-files https://github.com/tru-ID/auth0-passwordless-authentication-react-native.git

If the finished code is what you're interested in, then the command below will clone the repository with the main branch as the active branch:

git clone -b main https://github.com/tru-ID/auth0-passwordless-authentication-react-native.git
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 auth0-rn --project-dir .
Your project will need a backend server for your mobile application to communicate with. We've created the dev-server for you to get started with this tutorial as quickly as possible. The development server is written in Javascript and is not production ready, so it should only be used to understand the flow of running a Check.In your Terminal, navigate to the auth0-rn directory and run the following command to clone the dev-server:
git clone git@github.com:tru-ID/dev-server.git
In the dev-server directory, run the following command to create a.env copy of .env.example:
cp .env.example .env
Open this new .env file, update the values of TRU_ID_CLIENT_ID andTRU_ID_CLIENT_SECRET with your client_id and client_secret in yourtru.json file.
You'll need to expose your dev-server to the Internet for your mobile application to access your backend server. For this tutorial, we're using ngrok, which we've included in the dev-server functionality. So to start with, uncomment the following and populate them with your ngrok credentials:
NGROK_ENABLED=true
#NGROK_SUBDOMAIN=a-subdomain # Uncommenting this is optional. It is only available if you have a paid ngrok account.
NGROK_AUTHTOKEN=<YOUR_NGROK_AUTHTOKEN> # This is found in the ngrok dashboard: https://dashboard.ngrok.com/get-started/your-authtoken
NGROK_REGION=eu # This is where your ngrok URL will be hosted. If you're using tru.ID's `eu` data residency; leave it as is. Otherwise, you could specify `in` or `us`.
Run the development server; first, you'll need to install third-party dependencies. In the dev-server directory, run the following two commands:
npm install # Installs all third-party dependencies in package.json
npm run dev # Starts the server
Open up the URL shown in the terminal, which will be in the format in your desktop web browser to check that it is accessible.With the development server setup, we can move on to building the application.

Setting up Auth0

Sign in to your account at Auth0, redirecting you to the dashboard when authenticated. The dashboard is similar to what's shown in the example below:

A screenshot of the Auth0 Dashboard, displaying a Getting started flow.

Create a new Auth0 application by clicking on the ‘applications’ pane on the left-hand side. Selecting this will route you to a new page. Click on the ‘Create application’ button to create a new application, as shown below:

A screenshot of the Auth0 'Create application' dialog, with an input for the application's name, then four large boxes each displaying a type of application required, from native, single-page web application, regular web applications, and machine to machine applications.

Copy or rename the file .env.example to .env. Then within your Auth0 application, find your Client ID and Auth0 Domain and add the values of these two fields to the relevant variables within the .env file.

Enable passwordless authentication on Auth0

To enable passwordless authentication on Auth0, navigate to ‘Advanced Settings’, then ‘Grant Types’, and select ‘Passwordless OTP’ as shown in the screenshot below:

A screenshot of the Auth0 application Advanced Settings, with the tab 'Grant Types' selected, to display where to choose 'Passwordless OTP' as an option.

Next, hover over the ‘Authentication’ icon (on the dashboard to your left) and click ‘Passwordless’. The page loaded looks similar to what's shown in the example below:

A screenshot of the Auth0 application Passwordless Connections settings tab, allowing users to enable SMS or Email as an option.

Click on SMS and input the credentials as shown below. Auth0 uses Twilio, so be sure to grab the credentials from the Twilio Console.

A screenshot of an Auth0 application with the SMS option of Passwordless Connections tab open and the settings tab for SMS, which is defaulted to Twilio.

Once you've set up your Twilio credentials, navigate to the ‘Applications’ pane on your Auth0 dashboard and enable your application as shown below:

A screenshot of the Auth0 dashboard, with the SMS Twilio dialog open asking users to define which application to enable the SMS Passwordless authentication on.

Start the project

To start your mobile application, first install dependencies with the following command:

npm install

Depending on which physical device you're using, first, ensure you have a physical device connected, then run one of the two commands below:

npm run android
#or
npm run ios

The application on your mobile device, when successfully installed, looks similar to what's shown in the image below:

A tru.ID mobile app screen with the tru.ID logo, a phone number text input, and a submit button.

Get the user's phone number on the mobile device

The first step is to add the state management and UI required to grab the phone number, which is needed for authenticating with Auth0 and required to perform the SIM Check.

Open the file src/App.js, and at the top of this function, add the following four lines:

const [phoneNumber, setPhoneNumber] = useState('')
const [code, setCode] = useState('')
const [otpSent, setOtpSent] = useState(false)
const [loading, setLoading] = useState(false)

Now update the contents of the return() with the following:

return (
<SafeAreaView style={styles.container}>
<View style={styles.content}>
<Image style={styles.logo} source={require('./images/tru-logo.png')} />
{otpSent === false ? (
<View style={styles.content}>
<Text style={styles.heading}>Login</Text>
<TextInput
style={styles.textInput}
placeholder="Number ex. +448023432345"
placeholderTextColor="#d3d3d3"
keyboardType="phone-pad"
value={phoneNumber}
editable={!loading}
onChangeText={(value) => setPhoneNumber(value.replace(/\s+/g, ''))}
/>
{loading ? (
<ActivityIndicator
style={styles.spinner}
size="large"
color="#00ff00"
/>
) : (
<TouchableOpacity onPress={loginHandler} style={styles.button}>
<Text style={styles.buttonText}>Login</Text>
</TouchableOpacity>
)}
</View>
) : (
<View style={styles.content}>
<Text style={styles.heading}>Enter OTP</Text>
<TextInput
style={styles.textInput}
placeholder="Enter OTP Code"
placeholderTextColor="#d3d3d3"
keyboardType="default"
value={code}
editable={!loading}
onChangeText={(value) => setCode(value.replace(/\s+/g, ''))}
/>
{loading ? (
<ActivityIndicator
style={styles.spinner}
size="large"
color="#00ff00"
/>
) : (
<TouchableOpacity onPress={otpHandler} style={styles.button}>
<Text style={styles.buttonText}>Submit code</Text>
</TouchableOpacity>
)}
</View>
)}
</View>
</SafeAreaView>
)

Here you added the UI and state necessary. If the otpSent value is true, a different UI is rendered for submitting the OTP.

NOTE Make sure to replace the value of baseURL with your personal Ngrok URL in the format: https://{subdomain}.{region}.ngrok.io

The application on your mobile device should now look like this:

A tru.ID mobile app screen with the tru.ID logo, a phone number text input, and a submit button.

Create reusable error handlers

The next thing to do is create two reusable error handlers to address the two possible scenarios:

  • a successful scenario
  • and an error scenario.

Above the empty loginHandler definition in src/App.js, add the following two functions; errorHandler and successHandler:

const errorHandler = ({ title, message }) => {
return Alert.alert(title, message, [
{
text: 'Close',
onPress: () => console.log('Alert closed'),
},
])
}
const successHandler = () => {
Alert.alert('Login Successful', '✅', [
{
text: 'Close',
onPress: () => console.log('Alert closed'),
},
])
}

The errorHandler function takes in a title and a message prop, which it then uses to render an Alert to the screen with those values.

The successHandler renders an Alert that the check was successful.

Handle user sign in

The next step is to handle the user's attempt to sign in. The application uses Passwordless Login with Auth0 to achieve this.

Replace your empty loginHandler function with the following functionality:

const loginHandler = async () => {
setLoading(true)
try {
await auth0.auth.passwordlessWithSMS({
phoneNumber,
})
setOtpSent(true)
setLoading(false)
} catch (e) {
setLoading(false)
return errorHandler({
title: 'Something went wrong',
message: e.message,
})
}
}

Here, you set the value of loading to true, which serves as a visual indicator that something (an HTTP request) is taking place. Then the passwordlessWithSMS function is called, passing the phoneNumber, which sends an OTP to the number passed.

If there is a problem sending the OTP, the application catches the error and updates the UI to display this error. An example of the UI is shown in the image below:

A tru.ID mobile app screen with the tru.ID logo, an OTP text input, and a submit button.

The next step is to submit the OTP for verification. Locate the empty otpHandler function and populate it with the following contents:

const otpHandler = async () => {
try {
const result = await auth0.auth.loginWithSMS({
phoneNumber: phoneNumber,
code,
})
if (result) {
setLoading(false)
return successHandler()
}
} catch (e) {
console.log(JSON.stringify(e))
setLoading(false)
return errorHandler({
title: 'Something went wrong',
message: e.message,
})
}
}

The application submits the OTP and the user's phone number in the code above. The authentication request passes if the OTP and the user's phone number are linked, and the successHandler function gets called.

A failed authentication request means the OTP and the user's phone number are not linked, so the errorHandler function will be called to render a failed check.

A failed login will look similar to the example shown in the image below:

A tru.ID mobile app screen with the tru.ID logo, an OTP text input, a submit button, and a modal overlaid with the text 'something went wrong' and an error message.

Adding SIM card-based authentication to our existing workflow

You've now successfully implemented functionality into the mobile application to authenticate with Auth0. The next step is to add tru.ID's SIM swap detection into the sign-up flow, using the SIMCheck API.

First, confirm the user's mobile phone supports the SIMCheck API, check whether the connection is a mobile data connection, and check whether tru.ID supports the user's mobile network operator. If these checks pass, the application proceeds to make a request to the API.

Install the tru.ID React Native SDK, which contains this functionality:

npm install @tru_id/tru-sdk-react-native

Add the import of the tru.ID SDK at the top of src/App.js:

import TruSdkReactNative from '@tru_id/tru-sdk-react-native'

Replace the contents of the loginHandler function with what's shown in the example below:

const loginHandler = async () => {
setLoading(true)
try {
const reachabilityResponse = await TruSdkReactNative.openWithDataCellular(
'https://{DATA_RESIDENCY}.api.tru.id/public/coverage/v0.1/device_ip'
);
console.log(reachabilityResponse);
let isMNOSupported = false
if ('error' in reachabilityResponse) {
errorHandler({
title: 'Something went wrong.',
message: 'MNO not supported',
})
setLoading(false)
return
} else if ('http_status' in reachabilityResponse) {
let httpStatus = reachabilityResponse.http_status;
if (httpStatus === 200 && reachabilityResponse.response_body !== undefined) {
let body = reachabilityResponse.response_body;
console.log('product => ' + JSON.stringify(body.products[0]));
isMNOSupported = true;
} else if (httpStatus === 400 || httpStatus === 412 || reachabilityResponse.response_body !== undefined) {
errorHandler({
title: 'Something went wrong.',
message: 'MNO not supported',
})
setLoading(false)
return
}
}
let isSimCheckSupported = false
if (isMNOSupported === true) {
reachabilityResponse.response_body.products.forEach((product) => {
console.log('supported products are', product)
if (product.product_name === 'SIM Check') {
isSimCheckSupported = true
}
})
}
// If the SIMCheck API is supported, proceed with SIMCheck verification
if (isSIMCheckSupported) {
// SIM hasn't changed within 7 days, proceed with Auth
try {
await auth0.auth.passwordlessWithSMS({
phoneNumber,
})
setOtpSent(true)
setLoading(false)
} catch (e) {
console.log(JSON.stringify(e))
setLoading(false)
return errorHandler({
title: 'Something went wrong',
message: e.message,
})
}
} else {
// We don't support SIMCheck so just proceed with Auth0
try {
await auth0.auth.passwordlessWithSMS({
phoneNumber,
})
setOtpSent(true)
setLoading(false)
} catch (e) {
setLoading(false)
return errorHandler({
title: 'Something went wrong',
message: e.message,
})
}
}
} catch (e) {
setLoading(false)
errorHandler({ title: 'Something went wrong', message: e.message })
}
}

In the code sample above, the Reachability API gets called. This API request returns the network_id, network_name, country_code, and supported products. The products array is an optional array displaying which products the MNO supports. There is also an error object which may contain any potential errors returned in the reachability check.

Next, the application creates the variable isSIMCheckSupported, defaulting it to false. If the error status is not a 412 (IP Address or MNO is not supported), the application loops through the products and checks whether the product_name equals Sim Check. If it does, the value of isSIMCheckSupported is set to true.

If the isSIMCheckSupported variable remains false by the end of the loop, the SIMCheck is not supported, so the application skips a SIMCheck, and proceeds with Auth0 authentication. If isSIMCheckSupported is true, a SIMCheck can be created.

Above the App definition, create a function used for initiating the SIMCheck as shown below:

const createSIMCheck = async (phoneNumber) => {
const body = { phone_number: phoneNumber }
console.log('tru.ID: Creating SIMCheck for', body)
const response = await fetch(`${baseURL}/sim-check`, {
body: JSON.stringify(body),
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
const json = await response.json()
return json
}

Next, update the loginHandler function to use the method inside the if (isSimCheckSupported) { check:

...
if (isSimCheckSupported) {
const data = await createSIMCheck(phoneNumber)
if (data.no_sim_change !== false) {
setLoading(false)
return errorHandler({
title: 'Something went wrong',
message: 'SIM changed too recently. Please contact support.',
})
} else {
// SIM hasn't changed within 7 days, proceed with Auth
try {
await auth0.auth.passwordlessWithSMS({
phoneNumber,
})
setOtpSent(true)
setLoading(false)
} catch (e) {
console.log(JSON.stringify(e))
setLoading(false)
return errorHandler({
title: 'Something went wrong',
message: e.message,
})
}
}
}

Here, the application calls the createSIMCheck function, which makes a POST request to the sim-check endpoint. If the user's SIM has changed recently, an alert is displayed to the user, notifying the user of this. If the SIM hasn't changed, the application performs the Auth0 passwordless authentication.

The complete workflow, if successful, will look as follows:

A tru.ID mobile app screen with the tru.ID logo, a phone number text input, and a submit button.
A tru.ID mobile app screen with the tru.ID logo, a phone number text input, and a loading indicator.
A tru.ID mobile app screen with the tru.ID logo, an OTP text input, and a submit button.
A tru.ID mobile app screen with the tru.ID logo, an OTP text input, a submit button, and a modal overlayed with the title 'Login successful' and a green checkmark.

Wrapping up

There you have it! You've successfully added SIM swap detection to your React Native app authenticated with Auth0!

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.