tru.ID logo
LoginSignup

SIM Card Based Mobile Authentication with React Native & Amazon Cognito

In this tutorial, you'll learn how to add SIM card based mobile authentication to a React Native application signup workflow with Amazon Cognito. You'll use tru.ID PhoneCheck to verify the phone number associated with the SIM card on a mobile device.

Timothy Ogbemudia

Developer Experience Engineer

Last updated: 31 August 2021

tutorial cover image

Amazon Cognito is an identity provider which allows you to easily add login and sign-up to your applications, along with access control. However, there are problems with email and password, so it is imperative to be able to verify the user via a possession factor (i.e. phone number). If the phone number cannot be verified, it could indicate that the user is not who they claim to be. This is where the tru.ID PhoneCheck API comes in.

The tru.ID PhoneCheck API confirms the ownership of a mobile phone number by verifying the possession of an active SIM card with the same number. A mobile data session is created to a unique Check URL for the purpose of this verification. tru.ID then resolves a match between the phone number being verified and the phone number that the mobile network operator identifies as the owner of the mobile data session.

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

Getting Started

Firstly, clone the starter-files branch via:

$ git clone -b starter-files --single-branch https://github.com/tru-ID/amazon-cognito-sim-authentication.git

Install dependencies via:

$ npm install

Create a tru.ID account.

Install the tru.ID CLI via:

$ npm i -g @tru_id/cli

Set up the CLI with the tru.ID credentials which can be found within the tru.ID console.

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

Install the tru.ID CLI development server plugin.

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

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

Run the development server, pointing it to the directory containing the newly created project configuration. 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 .

Setting up Amazon Cognito

Create or log into an AWS account.

Navigate to the AWS Management portal.

Search for ‘cognito’, and select the service as shown below.

Cognito Service

You'll be taken to the following page. Select ‘manage user pools’ and create a new user pool.

Cognito Home

Give the pool a name and select ‘step through settings’ as shown below.

Cognito Pool Name

Under ‘attributes’ select ‘Email address or phone number’ as shown below.

Cognito Home

Under the ‘Which standard attributes do you want to require?’ heading, select ‘phone number’. Click ‘Next Step’ to continue.

Under ‘policies’, keep the defaults, as shown below.

Cognito Policies

Under ‘MFA and verifications’, keep the defaults, except under the ‘Which attributes do you want to verify?’ heading. There, select ‘No verification’, as shown below.

Cognito Home

Under 'Message customizations', keep the defaults and proceed to the next step.

Under 'Tags', simply proceed to the next step.

Under 'Devices', keep the default and proceed to the next step.

Under 'App clients', click 'Add an app client'. Give the App client a name. Be sure to deselect the 'Generate client secret' checkbox, athe AWS SDK doesn't support apps that have a client secret, as shown below.

Cognito App client name

Below, under ‘Auth Flows Configuration’, be sure to check 'Enable username password based authentication (ALLOW_USER_PASSWORD_AUTH)'.

You will then be informed that you have added an app client, as shown below. Click 'Next step' to proceed.

Cognito App client name

Under 'Triggers', leave the defaults and proceed to the next step.

Finally, under 'Reviews', you will be shown a recap of all your settings and allowed to modify them and create the pool. Once you've looked over your settings, create the pool. An example is shown below.

Cognito App client name

You should then be given a Pool id and Pool ARN. Open the terminal and run the following:

$ cp .env.example .env

Replace AMAZON_USER_POOL_ID with the Pool id value and AMAZON_CLIENT_ID with the app client id obtained from the 'General Settings > 'App clients' value.

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 on startup.

First onboarding screen UI
Second onboarding screen UI
Starter screen UI

Get the User's Credentials on the Mobile Device

The first step is to add the state management and UI required to grab the credentials we set up in our AWS console, which are email (as username), password, and phone_number.

Head over to src/screens.js and replace the Screens definition with the following:

const Screens = () => {
// replace with subdomain gotten from tru.ID localTunnel URL
const baseURL = 'https://{subdomain}.loca.lt'
const { setShowApp, showApp } = useContext(screenContext)
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [phoneNumber, setPhoneNumber] = useState({
Name: 'phone_number',
Value: '',
})
const [loading, setLoading] = useState(false)
const registerHandler = async () => {}
return (
<>
{!showApp ? (
<Onboarding
NextButtonComponent={NextButton}
DoneButtonComponent={DoneButton}
bottomBarHighlight={false}
onDone={() => setShowApp(true)}
pages={[
{
backgroundColor: '#FF8C00',
title: 'Blazingly Fast',
subtitle: 'Up to 50x faster than alternatives.',
image: <Image source={require('./images/man-on-phone.png')} />,
},
{
backgroundColor: '#00FF7F',
title: 'Get Started',
subtitle: 'Create an account to get started.',
image: <Image source={require('./images/woman-on-phone.png')} />,
},
]}
/>
) : (
<View style={styles.container}>
<View style={styles.images}>
<Image source={require('./images/tru-id-logo.png')} />
<Text style={styles.plus}>+</Text>
<Image source={require('./images/aws-cognito-logo.png')} />
</View>
<View style={styles.form}>
<View style={styles.center}>
<TextInput
style={styles.textInput}
placeholder="Email"
placeholderTextColor="#d3d3d3"
keyboardType="email-address"
value={email}
editable={!loading}
onChangeText={(Value) => setEmail(Value)}
/>
<TextInput
style={styles.textInput}
placeholder="Password"
placeholderTextColor="#d3d3d3"
value={password}
editable={!loading}
onChangeText={(Value) => setPassword(Value)}
secureTextEntry
/>
<TextInput
style={styles.textInput}
placeholder="Number ex. +448023432345"
placeholderTextColor="#d3d3d3"
keyboardType="phone-pad"
value={phoneNumber.Value}
editable={!loading}
onChangeText={(Value) =>
setPhoneNumber((prevState) => ({
...prevState,
Value: Value.replace(/\s+/g, ''),
}))
}
/>
{loading ? (
<ActivityIndicator
style={styles.spinner}
size="large"
color="#00ff00"
/>
) : (
<TouchableOpacity
onPress={registerHandler}
style={styles.button}
>
<Text style={styles.buttonText}>Sign Up</Text>
</TouchableOpacity>
)}
</View>
</View>
</View>
)}
</>
)
}

Our UI will look like this after the above changes:

Form screen UI

Handling User Registration

The user will begin registration by touching the TouchableOpacity, which triggers an onPress event. This event is bound to the registerHandler function.

First, let's install the AWS Cognito identity JS package that we'll use in the registerHandler function.

$ npm install --save amazon-cognito-identity-js

Next, add relevant imports to the top of src/screens.js:

import { AMAZON_USER_POOL_ID, AMAZON_CLIENT_ID } from '@env'
import {
CognitoUserPool,
CognitoUserAttribute,
} from 'amazon-cognito-identity-js'

The first import allows us to use the environment variables we defined earlier in our application, and the second contains imports relevant to Amazon Cognito.

Next, let's create a reusable function to handle any errors above the registerHandler 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 registerHandler function:

const registerHandler = async () => {
console.log('Register handler triggered')
const cognitoAttributeList = []
const userPool = new CognitoUserPool({
UserPoolId: AMAZON_USER_POOL_ID,
ClientId: AMAZON_CLIENT_ID,
})
console.log(userPool)
// pass extra attribute `phoneNumber` state into `CognitoUserAttribute`
const attributePhoneNumber = new CognitoUserAttribute(phoneNumber)
// push Cognito User Attributes into `cognitoAttributeList`
cognitoAttributeList.push(attributePhoneNumber)
setLoading(true)
console.log('AWS: signUp()')
userPool.signUp(
email,
password,
cognitoAttributeList,
null,
async (error, result) => {
console.log(error, result)
if (error) {
setLoading(false)
errorHandler({
title: 'Something went wrong.',
message: error.message,
})
return
}
console.log('AWS userPool signUp Result:', result)
},
)
}

Here, we create a new instance of CognitoUserPool passing in our AMAZON_USER_POOL_ID and AMAZON_CLIENT_ID.

Next, we pass an extra attribute phoneNumber into a new instance of CognitoUserAttribute.

We then push Cognito User Attribute into cognitoAttributeList.

We set loading to true, which gives the user a visual cue that a process (an HTTP network request) has kicked off as shown below:

loading screen UI

Afterwards, we sign up the user using the userPool.signUp method, passing the email, password, and cognitoAttributeList as parameters. If there is a problem, we call the generic error handler.

Augmenting the existing workflow to add SIM card based authentication

The last thing we'll do is add SIM card based mobile authentication to the signup flow using PhoneCheck.

For this, we make an HTTP POST request to the localtunnel URL + /phone-check, e.g. https://{subdomain}.loca.lt/phone-check. In a production environment, you would use your own server, but we'll use the development server running from the CLI in this case.

First, update the baseURL variable to the value of the development server localtunnel URL you started at the beginning of the tutorial.

const baseURL = 'https://{subdomain}.loca.lt'

Next, install the tru.ID React Native SDK:

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

Import to the top of src/screens.js:

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

With the SDK imported, we first want to check if the user's mobile operator supports the PhoneCheck API using the isReachable function.

Within the userPool.signUp callback function, underneath if(error){...} call the isReachable function within a try...catch:

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
}
} catch (err) {
setLoading(false)
errorHandler({
title: 'Something went wrong',
message: err.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 a PhoneCheck via a new function. Add the following:

async function createPhoneCheck(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
}

Here, we make a POST request to /phone-check which returns a check_url and check_id.

Within the userPool.signUp callback function, underneath if(!isPhoneCheckSupported){...} call the new function:

console.log('AWS userPool signUp Result:', result)
console.log('tru.ID: Creating PhoneCheck for', phoneNumber)
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 phoneCheck = await createPhoneCheck(phoneNumber.Value)
console.log('tru.ID: Created PhoneCheck', phoneCheck)
} catch (err) {
setLoading(false)
errorHandler({
title: 'Something went wrong',
message: err.message,
})
}

With the PhoneCheck created, we request the Check URL using the SDK to perform the authentication:

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 phoneCheck = await createPhoneCheck(phoneNumber.Value)
console.log('tru.ID: Created PhoneCheck', phoneCheck)
await TruSDK.check(phoneCheck.check_url)
} catch (err) {
setLoading(false)
errorHandler({
title: 'Something went wrong',
message: err.message,
})
}

The tru.ID React Native SDK forces a network request over mobile data, passing in the check_url which performs the authentication check and readies a result.

Get the result of the PhoneCheck via a new function:

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

Above, we make a GET request to /phone-check?check_id={checkId}, passing the checkId parameter as the check_id query parameter value.

Finally, use the result to determine the status of the phone verification:

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
}
const phoneCheck = await createPhoneCheck(phoneNumber.Value)
console.log('tru.ID: Created PhoneCheck', phoneCheck)
await TruSDK.openCheckUrl(phoneCheck.check_url)
const phoneCheckResult = await getPhoneCheck(phoneCheck.check_id)
setLoading(false)
if (phoneCheckResult.match) {
Alert.alert('Registration successful', '✅', [
{
text: 'Close',
onPress: () => console.log('Alert closed'),
},
])
} else {
errorHandler({
title: 'Registration Failed',
message: 'PhoneCheck match failed. Please contact support',
})
}
} catch (err) {
setLoading(false)
errorHandler({
title: 'Something went wrong',
message: err.message,
})
}

We render our generic error handler if there is no match or an error was caught. If none of those two conditions are true, we have a match and we render the result to the user.

The UI for a match would look like this:

loading screen UI

Wrapping Up

There you have it: you’ve successfully integrated tru.ID PhoneCheck with your React Native applications secured with Amazon Cognito.

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.