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: 16 November 2021
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 add login and sign-up to their applications easily.

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 actively verify the phone number, then 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'll also need the tru.ID CLI, which is used to create a project and run a local server for development. To install this CLI, run the following command in your terminal:

$ npm i -g @tru_id/cli

Set up the CLI with the tru.ID credentials which you can find within the tru.ID console.

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

For this tutorial, you'll be using the development server plugin, which runs a development server from the tru.ID CLI. Instructions to install the development server can be found on our GitHub page.

Now, create a new tru.ID project within the root directory via:

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

Run the development server, pointing it to the directory containing the newly created project configuration. This command 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. Please note the LocalTunnel URL, as you will need to use it later in this tutorial.

tru server -t

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 is the Supabase dashboard, a black background, On the page is a dark grey card, with the header 'Create a new project', then some 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 following page which shows settings for your project, such as the Site URL and Email Auth. Under ‘Email Auth’, toggle off ‘Enable Email Confirmations’, 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

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

Start the project

You've now 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 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 to determine 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 theirphone_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_LOCAL_TUNNEL_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 email and password fields on the form. 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 implemented a workflow to handle authentication requests using Supabase Auth successfully. 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 TruSDK 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
const reachabilityDetails = await TruSDK.isReachable()
console.log('Reachability details are', reachabilityDetails)
const info = JSON.parse(reachabilityDetails)
if (info.error && info.error.status === 400) {
errorHandler({
title: 'Something went wrong.',
message: 'Mobile Operator not supported',
})
setLoading(false)
return
}
let isPhoneCheckSupported = false
if (info.error && info.error.status !== 412) {
isPhoneCheckSupported = false
for (const { product_name } of info.products) {
console.log('supported products are', product_name)
if (product_name === 'Phone Check') {
isPhoneCheckSupported = true
}
}
} else {
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
}
}
}

The code above calls the isReachable function, 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, then it calls the errorHandler. Otherwise, the PhoneCheck is possible.

Now the application needs 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}/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)
await TruSDK.check(phoneCheckResponse.check_url)

The variable phoneCheckResponse returns a check_id and check_url values.

With the check_url opened, you now need to get the PhoneCheck result using the check_id value. To create a function for data fetching, beneath the function createPhoneCheck add the following:

const getPhoneCheck = async (checkId) => {
const response = await fetch(`${baseURL}/phone-check?check_id=${checkId}`)
const json = await response.json()
return json
}

Now in the signUpHandler find the line await TruSDK.check(phoneCheckResponse.check_url). Below this line, add the call to the newly created method:

const phoneCheckResult = await getPhoneCheck(phoneCheckResponse.check_id)
// 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

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