Biometric Authentication in React Native With Expo
#authentication #expo #react-native
4 min read
Here's a quick blog post on how to set up biometric authentication in your React Native + Expo app. Expo makes things super easy, so it's all pretty self explanatory really, but I wanted to write this blog post just to note down my process for someone using a "bare workflow".
Step 1: Install expo-local-authentication.
Check out the instructions here, but you basically need to run expo install expo-local-authentication
. Depending on the Expo SDK / React Native version you're on, you might need to install the react-native-unimodules
package as well.
If you've ejected from Expo, you'll need to explicitly state that you'll be using iOS's FaceID feature, along with the reason for using it. Add these lines (amended for your use case) to your Info.plist
file. Android permissions should be set automatically.
<key>NSFaceIDUsageDescription</key>
<string>This app uses Face ID to authenticate the user for blah blah blah reasons.</string>
Step 2: Create your authentication screen
The expo-local-authentication
package comes with some nice methods out of the box. The main ones I used were:
supportedAuthenticationTypesAsync
which determine what kinds of authentications are available on the deviceauthenticateAsync
which attempts to authenticate via Fingerprint/TouchID (or FaceID if available on the device).
The logical steps were therefore to:
- Call
supportedAuthenticationTypesAsync
upon the component screen mounting, to check what types of biometric authentication are available. This function returns an array of types, so I then looped through the results array and set the local state to eithertrue
orfalse
, forfacialRecognitionAvailable
,fingerprintAvailable
andirisAvailable
. - If at least one biometric authentication method is available, show a button, which on press, calls a function called
authenticate
. The function basically tries to authenticate the user usingauthenticateAsync
. The return value from this function is either "success" or "error", with the latter providing an error message. This can therefore be used to set and display theresult
variable for feedback to the user.
Side notes
- It's not possible to define a specific biometric authentication method for the
authenticateAsync
function. - If neither of the biometric authentication methods work, the function uses fallback methods like device pin code / password.
- If the error message comes back as unknown, a likely cause is that the user has tried authenticating too many times.
- The use of the enum
EResult
here is entirely optional, but just organises the code a little more nicely. ๐
import * as LocalAuthentication from 'expo-local-authentication';
import * as React from 'react';
// import your other components
enum EResult {
CANCELLED = 'CANCELLED',
DISABLED = 'DISABLED',
ERROR = 'ERROR',
SUCCESS = 'SUCCESS',
}
export function BiometricAuthScreen() {
const [facialRecognitionAvailable, setFacialRecognitionAvailable] = React.useState(false);
const [fingerprintAvailable, setFingerprintAvailable] = React.useState(false);
const [irisAvailable, setIrisAvailable] = React.useState(false);
const [loading, setLoading] = React.useState(false);
const [result, setResult] = React.useState<EResult>();
const checkSupportedAuthentication = async () => {
const types = await LocalAuthentication.supportedAuthenticationTypesAsync();
if (types && types.length) {
setFacialRecognitionAvailable(types.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION));
setFingerprintAvailable(types.includes(LocalAuthentication.AuthenticationType.FINGERPRINT));
setIrisAvailable(types.includes(LocalAuthentication.AuthenticationType.IRIS));
}
};
const authenticate = async () => {
if (loading) {
return;
}
setLoading(true);
try {
const results = await LocalAuthentication.authenticateAsync();
if (results.success) {
setResult(EResult.SUCCESS);
} else if (results.error === 'unknown') {
setResult(EResult.DISABLED);
} else if (
results.error === 'user_cancel' ||
results.error === 'system_cancel' ||
results.error === 'app_cancel'
) {
setResult(EResult.CANCELLED);
}
} catch (error) {
setResult(EResult.ERROR);
}
setLoading(false);
};
React.useEffect(() => {
checkSupportedAuthentication();
}, []);
let resultMessage;
switch (result) {
case EResult.CANCELLED:
resultMessage = 'Authentication process has been cancelled';
break;
case EResult.DISABLED:
resultMessage = 'Biometric authentication has been disabled';
break;
case EResult.ERROR:
resultMessage = 'There was an error in authentication';
break;
case EResult.SUCCESS:
resultMessage = 'Successfully authenticated';
break;
default:
resultMessage = '';
break;
}
let description;
if (facialRecognitionAvailable && fingerprintAvailable && irisAvailable) {
description = 'Authenticate with Face ID, touch ID or iris ID';
} else if (facialRecognitionAvailable && fingerprintAvailable) {
description = 'Authenticate with Face ID or touch ID';
} else if (facialRecognitionAvailable && irisAvailable) {
description = 'Authenticate with Face ID or iris ID';
} else if (fingerprintAvailable && irisAvailable) {
description = 'Authenticate with touch ID or iris ID';
} else if (facialRecognitionAvailable) {
description = 'Authenticate with Face ID';
} else if (fingerprintAvailable) {
description = 'Authenticate with touch ID ';
} else if (irisAvailable) {
description = 'Authenticate with iris ID';
} else {
description = 'No biometric authentication methods available';
}
return (
<Screen>
<Text>
{description}
</Text>
{facialRecognitionAvailable || fingerprintAvailable || irisAvailable ? (
<Button onPress={authenticate}>
Authenticate
</Button>
) : null}
{resultMessage ? <Text>{resultMessage}</Text> : null}
</Screen>
);
}