SIM Based Authentication with React Native using Supabase

In this tutorial, you'll learn how to add SIM card-based mobile authentication to a React Native application signup workflow with Supabase. You'll use tru.ID PhoneCheck to verify the phone number associated with the SIM card on a mobile device.
Timothy OgbemudiaDeveloper Experience Engineer
Last updated: 23 August 2022
tutorial cover image

Supabase is a platform as a service (PaaS) that provides all the backend tools you need to build apps as quickly and safely as possible. This service includes an authentication product, which is the focus of this tutorial. The authentication product allows developers to easily add login and sign-up to their applications.

However, because email and passwords are insecure, it is critical to be able to validate the user using a possession factor (i.e., a verified mobile phone number). If the application cannot verify the phone number, the user may not be who they say they are. tru.ID's PhoneCheck helps resolve this problem.

The tru.ID PhoneCheck API confirms the ownership of a mobile phone number by confirming the presence of an active SIM card with the same number. A mobile data session is created with a request to a Check URL unique to this SIM card. tru.ID then resolves a match between the phone number entered by the user and the phone number that the mobile network operator (MNO) identifies as the owner of the mobile data session.

Before you begin

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/supabase-passwordless-authentication.git

If you're only interested in the finished code in main, then run:

git clone -b main https://github.com/tru-ID/supabase-passwordless-authentication.git
A tru.ID Account is needed to make the PhoneCheck 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 supabase-sim-swap-detection --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 supabase-sim-swap-detection 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 Supabase

As previously stated, a Supabase account is needed. So, if you haven't already, create a Supabase account.

Once logged in, you'll see an interface like the one shown below:

A screenshot of a desktop browser having navigated to `app.supabase.io`. This is the Supabase dashboard, a black background, navigation on the left showing the `All Projects`. Then on the page is a green button with white text labelled `New Project`.

Select ‘new project’, which will take you to a page similar to the one shown below. On this page, enter your desired credentials for this project.

A screenshot of a desktop browser having navigated to create a new project. This page is the Supabase dashboard with a black background. On the page is a dark grey card, with the header 'Create a new project', input fields for the user to enter their project name, a database password, and the region for storing this information.

You'll then get redirected to the dashboard, where you can find and click on the icon on the left showing two people or the ‘Try Auth’ button.

A screenshot of a desktop browser having just created a new project. The header shows the project name in white text. Below are various cards for setting up the project.

You're then taken to the Project page, click on the Project's Settings link, and navigate to the Auth Providers section of the page. Under this, expand Email and toggle off Confirm Email, as shown below:

A screenshot of a desktop browser showing the new Supabase project settings. This page displays multiple input fields for the settings of the projects such as 'Site URL', 'JWT Expiry'.

Next, in your project, copy the file .env.example to .env:

cp .env.example .env

Update values of mthe following environment variables in this file:

  • SUPABASE_URL with the URL value found under ‘Settings > API > Config > URL’ in your project dashboard.
  • SUPABASE_PUBLIC_ANON with the anon public value found under ‘Settings > API > Project API keys > anon public’.

Start the project

You've completed the configuration and setup parts required to begin this tutorial. It's time to get started with the project. First, you need to install the dependencies. In your terminal, make sure you're currently in the project directory, and run the following command:

npm install

To test that everything is working from the start, make sure you have a physical device connected and run one of the following two commands, depending on which device you're testing with:

npm run android
#or
npm run ios

On your phone, you'll see a screen similar to the one displayed below:

A screenshot of a mobile app with a grey background. The tru.ID logo is centered at the top, then below this is a darker grey label with 'Sign Up'.

Get the user's credentials on the mobile device

The next step is determining how to receive a user's credentials on the mobile device. To store this information, you'll be using state management to the store, and some UI changes are also required for the user to input their information.

Authentication with Supabase Auth will require the user to input an email and their desired password. They'll also need to input their phone_number required to perform the PhoneCheck.

Open src/App.js and locate the line const App = () => {. Below this line, add the following additions for your state management:

const baseURL = '<YOUR_NGROK_URL>'
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const signUpHandler = async () => {}

Update the UI to support these bits of information in your store. Find the line <Text style={styles.heading}>Sign Up</Text>, and below this, add the following two TextInput, ActivityIndicator, and TouchableOpacity components:

<TextInput
style={styles.textInput}
placeholder="Email"
placeholderTextColor="#d3d3d3"
keyboardType="default"
value={email}
editable={!loading}
onChangeText={(value) => setEmail(value.replace(/\s+/g, ''))}
/>
<TextInput
style={styles.textInput}
placeholder="Password"
placeholderTextColor="#d3d3d3"
keyboardType="default"
secureTextEntry
value={password}
editable={!loading}
onChangeText={(value) => setPassword(value.replace(/\s+/g, ''))}
/>
{loading ? (
<ActivityIndicator
style={styles.spinner}
size="large"
color="#00ff00"
/>
) : (
<TouchableOpacity onPress={signUpHandler} style={styles.button}>
<Text style={styles.buttonText}>Sign Up</Text>
</TouchableOpacity>
)}

If you reload the app on your device, the screen will update to show what's displayed below:

the tru.ID logo with a text that says 'Sign Up' with an email text input, a password text input, and a button that says 'sign up'

Create reusable error handlers

The application needs two methods for handling different results:

  • The errorHandler function requires two parameters, the title and the message prop. It then renders an Alert to the screen using those props.
  • The successHandler renders an Alert.

Above the return in src/App.js, add the following two handler functions:

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'),
},
])
}

Currently, these two handlers don't get called in your application, but they are needed later in the tutorial.

Handle user signup

A third handler function is needed to handle the user's signup request. Superbase will be used to authenticate the user with their email and password. Find the line const signUpHandler = async () => {} and replace it with:

const signUpHandler = async () => {
const { session, error } = await supabase.auth.signUp({
email,
password,
})
if (!error && session) {
setLoading(false)
successHandler()
return
} else {
console.log(JSON.stringify(error))
setLoading(false)
errorHandler({ title: 'Something went wrong.', message: error.message })
return
}
}

The user signs in by inputting the form's email and password fields. The session and error responses are returned to determine the outcome of the request.

A check is made to see if there is an error and a session value returned to the signUp request. If there is no value in the error variable, the loading icon is no longer needed, and the code calls the successHandler function. Otherwise, the application calls the errorHandler.

A successful registration is shown in the image below:

the tru.ID logo with a text that says 'Sign Up' with an email text input and a password text input, a button that says 'sign up' and a modal overlaid with the text 'login successful' and a checkmark emoji.

Adding SIM card-based authentication to our existing workflow

So far, you've successfully implemented a workflow to handle authentication requests using Supabase Auth. The next step is to add SIM swap detection to the sign-up flow using the tru.ID PhoneCheck API.

First, you need to define the phoneNumber state and update the UI responsible for receiving the user's phone number. Find the line that defines the password state, and below this, add the definition for the phoneNumber state as shown below:

const App = () => {
...
const [phoneNumber, setPhoneNumber] = useState('')
...
}

In the return(), underneath the TextInput for the password, add a new component to take the user's phoneNumber. This component is shown below:

<TextInput
style={styles.textInput}
placeholder="Number ex. +448023432345"
placeholderTextColor="#d3d3d3"
keyboardType="phone-pad"
value={phoneNumber}
editable={!loading}
onChangeText={(value) => setPhoneNumber(value.replace(/\s+/g, ''))}
/>

Refreshing the app on the device will present you with an updated screen similar to the one shown below:

the tru.ID logo with a text that says 'Sign Up' with an email text input, a password text input, a phone number text input, and a button that says 'sign up'.

An API request to tru.ID's Reachability API confirms whether the user's mobile network operator supports PhoneCheck or not. This request is made easier with the use of the tru.ID React Native SDK, which you'll need to install with the following command:

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

Add the import of the tru.ID React Native SDK to the top of src/App.js:

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

Now, find your signUpHandler and replace the contents with what's shown below:

const signUpHandler = async () => {
setLoading(true)
// check if we have coverage using the `isReachable` function
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 isPhoneCheckSupported = false
if (isMNOSupported === true) {
reachabilityResponse.response_body.products.forEach((product) => {
console.log('supported products are', product)
if (product.product_name === 'Phone Check') {
isPhoneCheckSupported = true
}
})
}
// If the PhoneCheck API is supported, proceed with PhoneCheck verification and Supabase Auth
if (isPhoneCheckSupported) {
// proceed with Supabase Auth
const { session, error } = await supabase.auth.signUp({
email,
password,
})
if (!error && session) {
setLoading(false)
successHandler()
return
} else {
console.log(JSON.stringify(error))
setLoading(false)
errorHandler({ title: 'Something went wrong.', message: error.message })
return
}
} else {
const { session, error } = await supabase.auth.signUp({
email,
password,
})
if (!error && session) {
setLoading(false)
successHandler()
return
} else {
setLoading(false)
errorHandler({ title: 'Something went wrong.', message: error.message })
return
}
}
} catch (e) {
setLoading(false)
errorHandler({ title: 'Something went wrong', message: e.message })
}
}

The code above calls the Reachability API, which returns the network_id, network_name, country_code, and products. The products array is an optional array supported by the MNO. There is also an error object which contains any potential errors.

Next, the application creates the variable isPhoneCheckSupported. If the error status is not a 412, the application loops through the products and checks whether the product_name equals Phone Check; if it does, then isPhoneCheckSupported is set to true.

If the isPhoneCheckSupported variable is false by the end of the loop, the PhoneCheck is not supported. If isPhoneCheckSupported is false, it calls the errorHandler. Otherwise, the PhoneCheck is possible.

Now the application needs the functionality to create the PhoneCheck, so below successHandler, add the following createPhoneCheck function:

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

Now, inside the signUpHandler function, find the line if (isPhoneCheckSupported) {, and below this, add the following trigger to create a PhoneCheck:

const phoneCheckResponse = await createPhoneCheck(phoneNumber)
const checkResponse = await TruSdkReactNative.openWithDataCellular(
phoneCheckResponse.check_url
);

The variable phoneCheckResponse returns a check_id and check_url values.

With the check_url opened, you now need to complete the PhoneCheck flow, by making a PATCH request to tru.ID's PhoneCheck API. This request will return the result of the PhoneCheck. To create a function for data fetching, beneath the function createPhoneCheck, add the following:

const completePhoneCheck = async (checkId, code) => {
const response = await fetch(`${baseURL}/v0.2/phone-check/exchange-code`, {
method: 'POST',
body: JSON.stringify({
check_id: checkId,
code: code,
}),
headers: {
'Content-Type': 'application/json',
},
})
const json = await response.json()
return json
}

Now in the signUpHandler find the line where you openWithDataCellular on your check_url. Below this line, add the call to the newly created method:

const phoneCheckResult = await completePhoneCheck(checkResponse.response_body.check_id, checkResponse.response_body.code)
// if we do not have a match, do not proceed with Supabase auth
if (!phoneCheckResult.match) {
setLoading(false)
errorHandler({
title: 'Something Went Wrong',
message: 'PhoneCheck verification unsuccessful.',
})
return
}

The constant PhoneCheckResult returns a match value. If there is no match, the app renders the errorHandler and stops execution – it does not need to proceed with Supabase Auth.

If there is not a match, then the application will be displayed as shown in the image below:

the tru.ID logo with a text that says 'Sign Up' with an email text input, a password text input, and a phone number text input, a button that says 'sign up', a modal overlaid with the title 'Something went wrong', and a body that says 'PhoneCheck verification unsuccessful'

Wrapping up

That's it! With everything in place, you now have a seamless sign-up onboarding flow with minimal UX friction, resulting in reduced user drop-offs.

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.