Add silent device approval with Twilio Verify and React Native

January 28, 2022
Written by
Reviewed by

Add smooth, secure and silent authorization

What if you could provide ongoing authentication for your users without requiring a password or sending them a one-time passcode? With Twilio's Verify API, you can!

The Verify API includes a powerful open-source client library (SDK) for turning devices into secure keys. This allows your application to register trusted devices and use them as strong, phishing resistant authenticators. When the authentication is done on the registered device, everything can happen silently in the background without any user involvement. This lowers friction, increases usability, and still provides strong security.

Here's the flow you’ll be building in this tutorial:

  1. User is registered/authenticated (using SMS verification in our example, but could be username/password)
  2. Device is registered as a secure key ("Factor")
  3. User attempts to login
  4. Application silently authenticates user ("Challenge")

Device approval uses a similar workflow to push authentication but does not require any push notifications since this version happens in the background. This tutorial does not cover the Push Notification component, though you can absolutely enable that if you choose to do so.

The completed project is available on my GitHub if you want to skip ahead.

Prerequisites for building silent device approval

To code along with this post you'll need:

Set up your development environment

Set up the React Native CLI

For this tutorial you only need to set up one target operating system (Android or iOS). Follow the instructions under the React Native CLI Quickstart tab for your Development OS and Target OS of choice. Stop before "creating a new application".

The React Native Verify Push open source client library you’ll be using for silent approval is not compatible with Expo so you must use the React Native CLI setup.

Deploy two apps from Code Exchange

Before continuing, you’ll need a Verify Push sample backend, which you can deploy from our code exchange in a few clicks. Use the default "hash" value for identity processing, this will obfuscate any PII. To complete this step, you’ll need the Service SID from your Verify Service.

You'll also need to deploy this One-Time Passcode Verification app from Code Exchange in order for the phone verification piece to function. This app will use the same Verify Service SID. After deploying, click Go to live application, which will open a new tab in your browser. Keep this open for now.

Clone the starter application

You’ll need an application to build silent approval into and you can either use:

  1. An existing application
  2. The starter application based on the phone verification built in this blog post

If you already have an application to add silent authorization to, you can skip to the next step. If you don’t, keep reading to get the pre-built starter application.

In your terminal, navigate to a suitable working directory and run the following command:

git clone -b starter https://github.com/robinske/verify-push-silent-auth-react-native.git && cd verify-push-silent-auth-react-native

Next, install the dependencies with (npx pod-install is only required for iOS targets):

yarn install
npx pod-install

Open the folder you created with the above commands, verify-push-silent-auth-react-native. In this folder you will see a file called .env.example. Change the name of the file to just .env (remove the .example extension).

Open your .env file in your favorite text editor and look for the following highlighted line:

# Follow instructions to deploy this project
# https://www.twilio.com/code-exchange/one-time-passcode-verification-otp
# will look like https://verify-xxxx-xxxxxx.twil.io
BASE_URL=

Find your base URL from the One-Time Passcode app you deployed from Code Exchange. It will be the first part of the URL of the live application. It should look like https://verify-XXXX-bohzzb.twil.io.

Add this BASE_URL to your .env file (described in detail here).

Test your application

Test your application by running either yarn ios or yarn android, depending on your preferred target OS. If you run into any issues there are more details about building and running the app in this blog post.

How to register a device as a secure key: Verify Factors

The first thing you need to do is register the device. You can do this with the Verify Push client library, which will generate a key-pair on the device and send the public key to Twilio so you can just use the API instead of doing your own key management.

You do this by registering a Factor.

In the starter application that you cloned, open up src/screens/RegisterPush.tsx. This is the screen you’ll show once someone has signed up or authenticated and you trust them enough to let them register the device as a secure key. Good times to do this include at sign up or after login.

Replace the contents of the existing RegisterPush component with the following code:

const [spinner, setSpinner] = useState(false);
 const { phoneNumber } = route.params;
 return (
   <SafeAreaView style={commonStyles.wrapper}>
     <Spinner
       visible={spinner}
       textContent={"One moment..."}
       textStyle={commonStyles.spinnerTextStyle}
     />
     <Text style={commonStyles.prompt}>
       Secure your account with this device?
     </Text>
     <Text style={commonStyles.message}>
       Whenever there's a new login, we'll send a notification to this phone. It's safer than a text message and you can instantly approve or deny access.
     </Text>
     <TouchableOpacity
       style={{ backgroundColor: "#36D576", ...styles.button }}
       onPress={() => {
         setSpinner(true);
       }}
     >
       <Text style={commonStyles.buttonText}>Yes, use this device</Text>
     </TouchableOpacity>
     <TouchableOpacity
       style={{ backgroundColor: "#AEB2C1", ...styles.button }}
       onPress={() => console.log("skipping push registration")}
     >
       <Text style={commonStyles.buttonText}>Not now</Text>
     </TouchableOpacity>
   </SafeAreaView>
 );

This will render the following screen:

phone screen asking "secure your account with this device" with yes and no buttons

Hash identity to obfuscate PII

Next you need to actually handle when the user clicks Yes, use this device. Open up src/api/verify.js. You’re going to fill in the createFactor() function with the following steps:

This code uses the user’s phone number as the basis for the identity so you’ll want to obfuscate that. At the top of the file, right below the exists import lines, import the sha256 method:

import { sha256 } from "react-native-sha256";

Then add the following code inside the createFactor() function:

const identity = await sha256(phoneNumber);

Create Factor

The documentation shows you how to create a Factor and explains that you’ll need the following parameters:

  • factorName
  • verifyServiceSid
  • identity
  • pushToken
  • accessToken

You already have #2 and #3.

The Factor name can be any string but you can also use the react-native-device-info library to grab the Device Name (like "Kelley's iPhone 12"). Import the library (it should already be installed if you're using the starter project, otherwise follow the installation instructions):

import { getDeviceName, getDeviceToken } from "react-native-device-info";

Add the following to the createFactor() function:

const deviceName = await getDeviceName().catch(
 () => `${phoneNumber}'s Device'`
);

const deviceToken = await getDeviceToken().catch(
 () => "000000000000000000000000000000000000"
);

This will take care of both the factorName and the pushToken. The last thing you need is an access token to communicate with Twilio's API from the device.

If you haven't already, deploy this Verify Push Backend using your Verify Service SID. Open up your .env file and add the PUSH_BACKEND_URL.

# will look like https://verify-push-backend-1234-abcdef.twil.io
PUSH_BACKEND_URL=<your verify push backend url here>

Add the following inside the createFactor() function after hashing the identity:

const response = await fetch(`${PUSH_BACKEND_URL}/access-token`, {
 method: "POST",
 headers: {
   Accept: "application/json",
   "Content-Type": "application/json",
 },
 body: JSON.stringify({
   identity,
 }),
});

const json = await response.json();

At this point, you have everything we need to create the factor.

Add the Twilio Verify Push React Native Client Library (SDK)

From the terminal, install the Verify Push SDK:

yarn add https://github.com/twilio/twilio-verify-for-react-native.git

If your target is iOS also install the Pods:

npx pod-install

Back in verify.js, import the components you need to create a Factor:

import TwilioVerify, {
 PushFactorPayload,
 VerifyPushFactorPayload,
} from "@twilio/twilio-verify-for-react-native";

Add the following code to the createFactor() function:

const payload = new PushFactorPayload(
  deviceName,
  json.serviceSid,
  json.identity,
  deviceToken,
  json.token
);

let factor = await TwilioVerify.createFactor(payload);

Finally, immediately verify the Factor and store the Factor SID for future reference.

factor = await TwilioVerify.verifyFactor(
  new VerifyPushFactorPayload(factor.sid)
);

AsyncStorage.setItem("@factor_sid", factor.sid);
AsyncStorage.setItem("@identity", identity);

return factor.sid;

Here's what the final createFactor() function will look like:

export const createFactor = async (phoneNumber) => {
 // identity should not contain PII
 const identity = await sha256(phoneNumber);

 const response = await fetch(`${PUSH_BACKEND_URL}/access-token`, {
   method: "POST",
   headers: {
     Accept: "application/json",
     "Content-Type": "application/json",
   },
   body: JSON.stringify({
     identity,
   }),
 });

 const json = await response.json();

 const deviceName = await getDeviceName().catch(
   () => `${phoneNumber}'s Device'`
 );

 const deviceToken = await getDeviceToken().catch(
   () => "000000000000000000000000000000000000"
 );

 const payload = new PushFactorPayload(
   deviceName,
   json.serviceSid,
   json.identity,
   deviceToken,
   json.token
 );

 let factor = await TwilioVerify.createFactor(payload);
 factor = await TwilioVerify.verifyFactor(
   new VerifyPushFactorPayload(factor.sid)
 );

 AsyncStorage.setItem("@factor_sid", factor.sid);
 AsyncStorage.setItem("@identity", identity);

 return factor.sid;
};

Trigger device registration

Back in the RegisterPush.tsx file, update the Yes, use this device button to call your new createFactor() function.

Import the function:

import { createFactor } from "../api/verify";

Replace the onPress() method (inside the TouchableOpacity component right below the "​​Whenever there's a new login" message) with the following code:

onPress={() => {
 setSpinner(true);
 createFactor(phoneNumber)
   .then(() => {
     setSpinner(false);
     navigation.replace("Gated");
   })
   .catch((e) => {
     console.error(e);
   });
}}

Finally, in src/screens/Otp.tsx update the navigation to prompt for the device registration where it has the note // TODO - add a step to allow user to register device as a secure key:

- success && navigation.replace("Gated");
+ success && navigation.replace("RegisterPush", { phoneNumber });

At this point you can test out the Factor creation. Make sure you source your .env file to pick up new variables and restart the application with yarn ios or yarn android. Complete the phone verification and you should be able to click Yes, use this device and be rerouted to the banking image. Potential errors should be logged in the Metro window to help you debug any issues.

Silently authorize future logins

Now that the device is registered, you can use the device like a key instead of sending an SMS OTP or requiring a password.

This section will build the functionality behind the Login button on the welcome screen.

In src/screens/Welcome.tsx, look inside the Welcome() component function for the line that says // TODO add silent authorization. It should be somewhere around line 21. Replace this comment with the following navigation:

navigation.replace("Verifying")

Then head over to /src/api/verify.js to write your function to issue a Challenge. A Challenge is the Verify API's way of tracking a verification attempt. The client library will talk to the Verify API with your registered Factor to validate that the device is valid. Since you’re doing this all from the device, it can happen seamlessly in the background, hence the "silent" in silent authorization.

There are 4 steps to complete the silent authorization:

  1. Get identity from storage
  2. Create new challenge
  3. Immediately verify challenge
  4. Check challenge status to validate

Each of these steps will be described in the following sections.

Get identity from storage

The silentAuthorization() function expects the factorSid to be passed in, so you only need to fetch the identity from storage in order to create the challenge.

Inside of the try block in the silentAuthorization() function,  add the following code in place of the TODO comments:

const identity = await AsyncStorage.getItem("@identity");

const data = JSON.stringify({
 identity,
 factor: factorSid,
 message: "Login request",
});

Create new challenge

You also need the endpoint for creating a challenge. Fortunately the backend you used for the Access Token endpoint includes one. Create a challenge by calling the endpoint after you defined the data. Immediately after the code you added in the previous step, add the following:

const response = await fetch(`${PUSH_BACKEND_URL}/create-challenge`, {
 method: "POST",
 headers: {
   "Content-Type": "application/json",
 },
 body: data,
});

const json = await response.json();
const challengeSid = json.sid;

Immediately verify challenge

First, import the necessary components. At the top of the same verify.js file, edit your imports to add the following after VerifyPushFactorPayload:

UpdatePushChallengePayload,
ChallengeStatus,

Add the following code to the end of the try block in the silentAuthorization() function, below the code you added in the previous steps, to "update" the challenge - this is what will do the validation.

const payload = new UpdatePushChallengePayload(
 factorSid,
 challengeSid,
 ChallengeStatus.Approved
);
let updated = await TwilioVerify.updateChallenge(payload);
updated = await TwilioVerify.getChallenge(challengeSid, factorSid);

Check challenge status to validate

Finally, check the status to make sure it's approved and return the result:

return updated.status === ChallengeStatus.Approved;

Trigger silent authorization at login

Head over to /src/screens/Verifying.tsx to build the authorization check. There's some boilerplate for the component to give the user an idea about what's happening.

Verify Push Challenges are very secure and very fast. We recommend showing the user a message about the process so they know their account is protected.

You’ll be using a React Hook to trigger the verification on page load since you’re not requiring the user to click any more buttons.

Replace the // TODO with the following code:

useEffect(() => {
 AsyncStorage.getItem("@factor_sid")
   .then((factorSid) => {
     silentAuthorization(factorSid).then((approved) => {
       if (approved) {
         navigation.replace("Gated");
       }
     });
   })
   .catch((e) => {
     console.error(e);
     navigation.replace("PhoneNumber");
   });
}, [isFocused]);

The isFocused helper from the navigation library is necessary to tell the component there was a state change on page load and trigger the useEffect hook.

Reload the application and try clicking Login. You should briefly see the verification screen. Because it happens so fast you might consider building in a timeout to display the message to the user for a minimum of 1 or 2 seconds.

What about Push Notifications?

You used the Verify Push API to build this demo but didn't actually do anything with push notifications. You absolutely can add that and use the mobile device as a key for desktop logins, call center authentication, and more. Learn more about how to enable push notifications in the documentation.

One of the challenges with device authorization like this is how to manage account recovery: what happens when the end user loses their device? More developers are seeing the value of Push as a first level of frictionless defense and then will fallback to other channels like one-time passwords if the device is unavailable. The Verify API offers many channels for fallback.

If you have any questions about building with Verify Push or pricing, please get in touch. I can't wait to see what you build and secure!