tru.ID logo
LoginSignup

SIM Swap Detection with React Native Firebase Phone Authentication

In this tutorial, you'll learn how to add SIM swap detection to a React Native application authenticated with Firebase Phone Auth. You'll use SIMCheck to verify if the SIM card associated with a phone number has changed recently in order to detect potential SIM swap attacks.

Timothy Ogbemudia

Developer Experience Engineer

Last updated: 31 August 2021

tutorial cover image

In this tutorial we'll cover how to add SIM swap detection using tru.ID SIMCheck to a React Native application authenticated with Firebase Phone Auth.

The tru.ID SIMCheck API provides information on when a SIM card associated with a mobile phone number was last changed. This 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 2FA or anti-fraud workflows.

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

Getting Started

Clone the starter-files branch via:

$ git clone -b starter-files --single-branch https://github.com/tru-ID/firebase-phone-auth-sim-swap-detection.git

To install dependencies, run:

$ cd firebase-phone-auth-sim-swap-detection && npm install

Create a tru.ID Account and you'll end up on the Console containing CLI setup instructions.

Install the tru.ID CLI via:

$ npm i -g @tru_id/cli

Input your tru.ID credentials, which can be found within the tru.ID console.

Install the tru.ID CLI development server plugin:

$ tru plugins:install @tru_id/cli-plugin-dev-server

Create a new tru.ID project within the root directory via:

$ tru projects:create rn-firebase-auth --project-dir .

Run the development server. This will also open up a localtunnel to your development server, making it publicly accessible to the Internet, so that your mobile phone can access it when only connected to mobile data.

$ tru server -t --project-dir .

Open up the URL that is shown in the terminal, which will be in the format https://{subdomain}.loca.lt, in your desktop web browser, to check that it is accessible.

With the development server set up, we can move on to building the React Native application.

Setting up React Firebase Auth

This project uses React Native Firebase. So, first install both the app and auth dependencies:

$ npm install --save @react-native-firebase/app @react-native-firebase/auth

Then follow the guides within the official documentation:

These instructions are quite complicated, but it's better to follow the official documentation by Google and Apple here. However, please raise an issue on our GitHub repo if you get stuck or have any questions.

With all dependencies set up, we can now start building the React Native application.

Starting Project

To start the project, ensure you have a physical device connected (see Running React Native on a physical device guide ) then run:

$ npm run android
#or
npm run ios

Your app will look like this:

React Native Starter App

Get the User's Phone Number on the Mobile Device

Let's start by adding the UI and state management required for the user to input (TextInput) and submit their phone number (TouchableOpacity). We'll also conditionally render UI components depending on whether we have sent an (One Time Passcode) to the user or not.

const App = () => {
const [phoneNumber, setPhoneNumber] = React.useState('')
const [loading, setLoading] = React.useState(false)
const [sentCode, setSentCode] = React.useState(null)
const [code, setCode] = React.useState('')
// Replace `URL` with LocalTunnel URL in the format : https://{subdomain}.loca.lt
const URL = 'https://wonderful-lionfish-40.loca.lt'
// we'll handle the SIMCheck API Call and Firebase Phone Authentication in the function below
const onPressHandler = async () => {}
// we'll handle verifying the received OTP in the function below
const confirmationHandler = async () => {}
return (
<SafeAreaView style={styles.backgroundStyle}>
<StatusBar barStyle="light-content" />
<Image style={styles.logo} source={require('./images/tru-logo.png')} />
<Text style={styles.heading}>tru.ID + Firebase Auth</Text>
{sentCode ? (
<View style={styles.center}>
<TextInput
style={styles.textInput}
placeholder="OTP"
placeholderTextColor="#d3d3d3"
onChangeText={(text) => setCode(text)}
value={code}
/>
{loading ? (
<ActivityIndicator
style={styles.spinner}
size="large"
color="#00ff00"
/>
) : (
<TouchableOpacity
onPress={confirmationHandler}
style={styles.button}
>
<Text style={styles.buttonText}>Verify</Text>
</TouchableOpacity>
)}
</View>
) : (
<View style={styles.center}>
<TextInput
style={styles.textInput}
keyboardType="phone-pad"
placeholder="ex. +448023432345"
placeholderTextColor="#d3d3d3"
onChangeText={(text) => setPhoneNumber(text.replace(/\s+/g, ''))}
/>
{loading ? (
<ActivityIndicator
style={styles.spinner}
size="large"
color="#00ff00"
/>
) : (
<TouchableOpacity onPress={onPressHandler} style={styles.button}>
<Text style={styles.buttonText}>Login</Text>
</TouchableOpacity>
)}
</View>
)}
</SafeAreaView>
)
}

That application UI will now look as follows:

React Native Starter App

Handling Phone Authentication

The user will begin phone authentication by touching the TouchableOpacity. This event is then handled by the onPressHandler function and starts the Firebase Phone Auth workflow.

Let's import the Firebase React Native Auth package we'll use at the top of app.js:

import auth from '@react-native-firebase/auth'

Next, let's create a reusable function to handle any errors above the onPressHandler function:

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

This function takes in a title and message prop and renders an Alert to the screen using those props.

Now we can update the onPressHandler function:

const onPressHandler = async () => {
setLoading(true)
try {
console.log('Firebase: signInWithPhoneNumber')
const confirmation = await auth().signInWithPhoneNumber(phoneNumber)
console.log('Firebase: signInWithPhoneNumber result', confirmation)
setLoading(false)
setSentCode(confirmation)
} catch (e) {
setLoading(false)
errorHandler({
title: 'Something went wrong',
message: e.message,
})
}
}

Here we set loading to true, which gives the user a visual cue that a process (an HTTP network request) has kicked off.

We then attempt to sign in with the user's phone number via Firebase using the auth().signInWithPhoneNumber() function.

If we have successfully sent an OTP we set loading to false and call setCode, passing the signInWithPhoneNumber response. setCode will trigger the logic in the re-render to return the view with the OTP screen.

If an error occurs we render an Alert to the user with the error message.

Your app will look like this:

React Native Firebase Auth with OTP input

Handling OTP Validation

The next step is to handle the OTP the user inputs in the confirmationHandler function, which is called when the user submits the OTP:

const confirmationHandler = async () => {
try {
setLoading(true)
const resp = await sentCode.confirm(code)
setLoading(false)
if (resp) {
Alert.alert('Successfully logged in', '✅', [
{
text: 'Close',
onPress: () => console.log('Alert closed'),
},
])
}
} catch (e) {
console.error(e)
setLoading(false)
// set `sentCode` to null resetting the UI
setSentCode(null)
errorHandler({
title: 'Something went wrong',
message: e.message,
})
}
}

Once again, we set loading to true to indicate that some work is going on in the background. Then we use the sentCode object – set from the response from sending the OTP – and access the confirm function to validate the OTP the user entered.

If there's a response, which means the OTP the user entered is valid, we alert the user with a success message. If something went wrong (i.e. an error occurred), we set setCode to null, which resets the UI, and we inform the user.

When the OTP is valid, the app will look as follows:

React Native Starter App

Augmenting the existing workflow to add SIM Swap Detection

The last thing we have to do is add SIM Swap detection to the application's workflow using SIMCheck.

First thing we want to do is check if the users MNO supports the SIMCheck API. To do this, we'll use the tru.ID React Native SDK. In your Terminal, navigate to the project directory and run the following command to install the SDK:

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

Open App.js and add the import to the top of the file:

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

In order to check if the Mobile Network Operator (MNO) supports the SIMCheck API, we'll use the isReachable function. Update the onPressHandler to:

const onPressHandler = async () => {
try {
const reachabilityDetails = await TruSDK.isReachable()
const reachabilityInfo = JSON.parse(reachabilityDetails)
if (reachabilityInfo.error.status === 400) {
errorHandler({
title: 'Something went wrong.',
message: 'MNO not supported',
})
setLoading(false)
return
}
let isPhoneCheckSupported = false
if (reachabilityInfo.error.status !== 412) {
for (const { productType } of reachabilityInfo.products) {
console.log('supported products are', productType)
if (productType === 'PhoneCheck') {
isPhoneCheckSupported = true
}
}
} else {
isPhoneCheckSupported = true
}
if (!isPhoneCheckSupported) {
errorHandler({
title: 'Something went wrong.',
message: 'PhoneCheck is not supported on MNO',
})
return
}
console.log('Firebase: signInWithPhoneNumber')
const confirmation = await auth().signInWithPhoneNumber(phoneNumber)
console.log('Firebase: signInWithPhoneNumber result', confirmation)
setLoading(false)
setSentCode(confirmation)
} catch (e) {
console.error(e)
setLoading(false)
errorHandler({
title: 'Something went wrong',
message: e.message,
})
}
}

Here we call isReachable which returns a list of supported products by the MNO. If we get a 400 the MNO is not supoorted and we inform the user.

If the status is not 412 we loop through the returned list of products and if any match PhoneCheck we set isPhoneCheckSupported to true.

If isPhoneCheckSupported is false , we inform the user and stop execution.

Now we can create our SIMCheck. For this, we'll need to make an HTTP POST request to the localtunnel URL + /sim-check e.g. https://wonderful-lionfish-40.loca.lt/sim-check. In a production environment you'd use your own server, but we'll use the development server running from the CLI in this case.

Update the URL variable to the value of the development server localtunnel URL you started at the beginning of the tutorial.

const URL = 'https://wonderful-lionfish-40.loca.lt'

Finally, update the onPressHandler to make a request to check for a SIM Swap before executing the Firebase Phone Auth flow:

const onPressHandler = async () => {
setLoading(true)
try {
const reachabilityDetails = await TruSDK.isReachable()
const reachabilityInfo = JSON.parse(reachabilityDetails)
if (reachabilityInfo.error.status === 400) {
errorHandler({
title: 'Something went wrong.',
message: 'MNO not supported',
})
setLoading(false)
return
}
let isPhoneCheckSupported = false
if (reachabilityInfo.error.status !== 412) {
for (const { productType } of reachabilityInfo.products) {
console.log('supported products are', productType)
if (productType === 'PhoneCheck') {
isPhoneCheckSupported = true
}
}
} else {
isPhoneCheckSupported = true
}
if (!isPhoneCheckSupported) {
setLoading(false)
errorHandler({
title: 'Something went wrong.',
message: 'PhoneCheck is not supported on MNO',
})
return
}
const body = { phone_number: phoneNumber }
console.log('tru.ID: Creating SIMCheck for', body)
const response = await fetch(`${URL}/sim-check`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
const data = await response.json()
console.log('tru.ID: SIMCheck created', data)
if (data.no_sim_change === false) {
setLoading(false)
errorHandler({
title: 'SIM Change Detected',
message: 'SIM changed too recently. Please contact support.',
})
return
}
console.log('Firebase: signInWithPhoneNumber')
const confirmation = await auth().signInWithPhoneNumber(phoneNumber)
console.log('Firebase: signInWithPhoneNumber result', confirmation)
setLoading(false)
setSentCode(confirmation)
} catch (e) {
setLoading(false)
errorHandler({
title: 'Something went wrong',
message: e.message,
})
}
}

Here we make a POST request to the sim-check endpoint, and if the user's SIM has changed recently we display an Alert to the user. If the SIM hasn't changed, we perform the Firebase phone authentication.

Wrapping Up

There you have it: you’ve successfully integrated tru.ID SIMCheck with your React Native applications secured with Firebase Phone Authentication, and now you have SIM Swap detection for a more secure login flow.

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.