Intro
Face ID has become a widely used security feature in iOS & Android applications. This feature allows users to experience a seamless and secure authentication process when using your app.
We will be integrating Face ID authentication into a React Native application using react-native-biometrics
package.
Prerequisites
To follow the post, you will need a React Native application, which can be easily created using this command:
npx react-native init RealApp
For this example we will be using one of the WithFrame's pre-built sign in screens with the Face ID action.
Installation
If you are using yarn, run the following command:
yarn add react-native-biometrics
If you are using NPM, run the following command:
npm install react-native-biometrics --save
Also, let's not forget about linking the native packages:
npx pod-install
On React Native 0.60+ the CLI autolink feature links the module while building the app.
Permissions
After the installation is complete, we will need to add permissions strings for both iOS and Android.
For Android, you will need to add the following to your AndroidManifest.xml file:
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
For iOS, you will need to add the following to your Info.plist file:
<key>NSFaceIDUsageDescription</key>
<string>Enabling Face ID allows you quick access to RealApp</string>
Step 1: Create a biometric key pair
On our sign-in screen, we have two buttons, “Sign in” and “Face ID”. After a user's credentials are verified, we'll ask them if they would like to use a Face ID feature the next time.
Of course, we will have to check if the Face ID is available on the device first by using isSensorAvailable()
method.
As soon as the publicKey
is obtained using the createKeys()
method, we should send it to the server and save it on the user's entity. We will later use it to verify the signature.
import ReactNativeBiometrics, { BiometryTypes } from "react-native-biometrics";
<TouchableOpacity
onPress={async () => {
// Verify user credentials before asking them to enable Face ID
const { userId } = await verifyUserCredentials();
const rnBiometrics = new ReactNativeBiometrics();
const { available, biometryType } = await rnBiometrics.isSensorAvailable();
if (available && biometryType === BiometryTypes.FaceID) {
Alert.alert(
"Face ID",
"Would you like to enable Face ID authentication for the next time?",
[
{
text: "Yes please",
onPress: async () => {
const { publicKey } = await rnBiometrics.createKeys();
// `publicKey` has to be saved on the user's entity in the database
await sendPublicKeyToServer({ userId, publicKey });
// save `userId` in the local storage to use it during Face ID authentication
await AsyncStorage.setItem("userId", userId);
},
},
{ text: "Cancel", style: "cancel" },
]
);
}
}}
>
<View style={styles.btn}>
<Text style={styles.btnText}>Sign in</Text>
</View>
</TouchableOpacity>
Step 2: Verify biometric signature
Now that we have a publicKey
stored on a user's entity, we can use it to verify the user authentication.
<TouchableOpacity
onPress={async () => {
const rnBiometrics = new ReactNativeBiometrics();
const { available, biometryType } = await rnBiometrics.isSensorAvailable();
if (!available || biometryType !== BiometryTypes.FaceID) {
Alert.alert("Oops!", "Face ID is not available on this device.");
return;
}
const userId = await AsyncStorage.getItem("userId");
if (!userId) {
Alert.alert(
"Oops!",
"You have to sign in using your credentials first to enable Face ID."
);
return;
}
const timestamp = Math.round(new Date().getTime() / 1000).toString();
const payload = `${userId}__${timestamp}`;
const { success, signature } = await rnBiometrics.createSignature({
promptMessage: "Sign in",
payload,
});
if (!success) {
Alert.alert(
"Oops!",
"Something went wrong during authentication with Face ID. Please try again."
);
return;
}
const { status, message } = await verifySignatureWithServer({
signature,
payload,
});
if (status !== "success") {
Alert.alert("Oops!", message);
return;
}
Alert.alert("Success!", "You are successfully authenticated!");
}}
>
<View style={styles.btnSecondary}>
<MaterialCommunityIcons
color="#000"
name="face-recognition"
size={22}
style={{ marginRight: 12 }}
/>
<Text style={styles.btnSecondaryText}>Face ID</Text>
<View style={{ width: 34 }} />
</View>
</TouchableOpacity>
Step 3: Verifying the signature with the public key in NodeJS
After the user is prompted for their Face ID authentication, Apple will retrieve the private key from the keystore, and then use it to generate an RSA PKCS#1v1.5 SHA 256 signature.
Earlier, we saved a public key on the user's entity, and now we can use it to verify that the signature was signed using the private key from the same public/private key pair. In NodeJS, this can be accomplished using the crypto module.
const express = require("express");
const bodyParser = require("body-parser");
const crypto = require("crypto");
const app = express();
app.use(bodyParser.json({ type: "application/json" }));
app.post("/", async (req, res) => {
const { signature, payload } = req.body;
const userId = payload.split("__")[0];
const user = await getUserFromDatabaseByUserId(userId);
if (!user) {
throw new Error("Something went wrong during your Face ID authentication.");
}
// this is the public key that was saved earlier
const { publicKey } = user;
const verifier = crypto.createVerify("RSA-SHA256");
verifier.update(payload);
const isVerified = verifier.verify(
`-----BEGIN PUBLIC KEY-----\n${publicKey}\n-----END PUBLIC KEY-----`,
signature,
"base64"
);
if (!isVerified) {
return res.status(400).json({
status: "failed",
message: "Unfortunetely we could not verify your Face ID authentication",
});
}
return res.status(200).json({
status: "success",
});
});