To keep up with the latest technology fingerprint authentication has become extremely popular in mobile apps these days. It secures the app and makes it a seamless authentication flow for the user.
While implementing this we had to go through a lot of brainstorming and concluded the following flow which we’ll be covering in this blog. Also one thing to keep in mind is this flow is recommended for the single user device and not shared devices.
We’ll be using two libraries for this feature:
We’ve divided the implementation into TWO steps.
- Enable fingerprint from the application’s setting.
- Login via fingerprint.
Enable fingerprint from application setting
On the user’s action to “enable fingerprint” we’ll be checking if the fingerprint is supported by OS and device. For that we need to:
1. Install React-native-touch-id.
npm i --save react-native-touch-id
or
yarn add react-native-touch-id
2. Then link the library to the project
react-native link react-native-touch-id
On iOS, you can also link package by updating your podfile
pod 'TouchID', :path => "#{node_modules_path}/react-native-touch-id"
And then run
pod install
3. Add the following permissions to their respective files:
In your AndroidManifest.xml:
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
In your Info.plist:
<key>NSFaceIDUsageDescription</key>
<string>Enabling Face ID allows you quick and secure access to your account.</string>
Now create a service `biometricService.js` and add `checkBiometricSupportednEnrolled` method.
import TouchID from 'react-native-touch-id';
export const checkBiometricSupportednEnrolled = async () => {
const optionalConfigObject = {
unifiedErrors: false, // use unified error messages (default false)
passcodeFallback: false // if true is passed, it will allow isSupported to return an error if the device is not enrolled in touch id/face id etc. Otherwise, it will just tell you what method is supported, even if the user is not enrolled. (default false)
}
return new Promise((resolve, reject) => {
//isSupported returns both cases 1. if supported 2. Is enabled/configured/enrolled
TouchID.isSupported(optionalConfigObject)
.then(biometryType => {
// Success code.
// as we are focusing on fingerprint for now
if (biometryType && biometryType != 'FaceID') {
resolve(true);
} else {
let fingerprintLableForOS = Platform.OS == "ios" ? "Touch ID" : "Fingerprint";
reject( fingerprintLableForOS + " is not available on this device");
}
})
.catch(error => {
// iOS Error Format and android error formats are different
// android use code and ios use name
// check at https://github.com/naoufal/react-native-touch-id
let errorCode = Platform.OS == "ios" ? error.name : error.code;
if (errorCode === "LAErrorTouchIDNotEnrolled" || errorCode === "NOT_AVAILABLE" || errorCode === "NOT_ENROLLED") {
let fingerprintLableForOS = Platform.OS == "ios" ? "Touch ID" : "Fingerprint";
resolve(fingerprintLableForOS + " has no enrolled fingers. Please go to settings and enable " + fingerprintLableForOS + " on this device.");
} else {
reject(Platform.OS == "ios" ? error.message : translations.t(error.code));
}
});
});
}
This service method will return true if the fingerprint is supported and one or more fingerprints are already enrolled on the device else it will return error code. You can handle different types of errors according to your case. For now, we just want to handle LAErrorTouchIDNotEnrolled
for iOS, NOT_ENROLLED
for android, which means fingerprint is supported but there are no enrolled fingers. We’ll also show redirection alert [redirect to device settings] on these errors.
Call this service
let isFingerPrintSupported = yield call(KeychainService.checkBiometricSupportednEnrolled);
and add
if (isFingerPrintSupported === true) {
//fingerprint is supported and enrolled
//TODO: we’ll work here in the next step
} else {
//show alert "TouchID has no enrolled fingers. Please go to settings and enable fingerprint on this device." that we returned from the service
Alert.alert(
"Alert",
isFingerPrintSupported,
[{
text: 'Ok', onPress: () => {
//redirect to settings
Platform.OS === "ios"
? Linking.openURL('app-settings:')
: AndroidOpenSettings.securitySettings() // Open security settings menu
}
}]
);
On getting supported and enrolled responses from the service we need to save current user’s credentials to Keystore [for android] or keychain [for iOS].
For this purpose, we need to install React-native-keychain.
yarn add react-native-keychain
react-native link react-native-keychain
Re-build your Android and iOS projects and create a new service `keychainService.js` and add method `setCredentials`.
import * as Keychain from 'react-native-keychain';
import { Platform } from "react-native";
export const setCredentials = async (username, password) => {
return new Promise((resolve, reject) => {
// Store the credentials
Keychain.setGenericPassword(username, password)
.then(resp => {
resolve(true)
})
.catch(err => {
console.log("err: ", err);
reject(err);
});
});
}
Now we’ll call this function from the `if` condition we left empty earlier.
if (isFingerPrintSupported === true) {
yield call(KeychainService.setCredentials, user_name, JSON.stringify({password}));
}
setGenericPassword is limited to strings only, so if you need to store objects, etc, please use JSON.stringify/JSON.parse when you store/access it.
Till now our first section is completed. You can play around with both of these libraries and use it for faceID too.
Authenticate via fingerprint
On application launch, if the user is not logged in already we’ll check if our keychain contains login credentials or not. For that, we need to add another function in our `keychainService.js` as `getCredentials` which will return saved keychain credentials.
export const getCredentials = async () => {
return new Promise((resolve, reject) => {
Keychain.getGenericPassword()
.then((credentials) => {
if (credentials && credentials.username) {
// console.log('Credentials successfully loaded for user ' + credentials.username);
resolve(credentials);
} else {
// console.log('No credentials stored');
resolve(null);
}
})
.catch(err => {
console.log("err: ", err);
reject(err);
});
});
}
And call this function by dispatching action in your login’s componentWillMount.
let credentials = yield call(KeychainService.getCredentials);
if (credentials && credentials.username)) {
let isFingerPrintSupported = yield call(KeychainService.checkBiometricSupportednEnrolled);
if (isFingerPrintSupported === true) {
// show fingerprint alert on login page
// and authenticate FingerPrint when user touch the sensor
}
} else {
// else don’t show fingerprint option on login
}
Use `checkBiometricSupportednEnrolled` to check if the fingerprint is still enrolled and show login via fingerprint modal on the login screen.
Now there can be two ways in which the user can log in:
1. Login with fingerprint
We need to authenticate the user’s fingerprint when he touches the sensor. For this make a function in our `biometricService.js` as `authenticateFingerPrint`.
export const authenticateFingerPrint = () => {
return new Promise((resolve, reject) => {
// configuration object for more detailed dialog setup and style:
// const optionalConfigObject = {
// title: 'Authentication Required', // Android
// imageColor: '#e00606', // Android
// imageErrorColor: '#ff0000', // Android
// sensorDescription: 'Touch sensor', // Android
// sensorErrorDescription: 'Failed', // Android
// cancelText: 'Cancel', // Android
// fallbackLabel: 'Show Passcode', // iOS (if empty, then label is hidden)
// unifiedErrors: false, // use unified error messages (default false)
// passcodeFallback: false, // iOS - allows the device to fall back to using the passcode, if faceid/touch is not available. this does not mean that if touchid/faceid fails the first few times it will revert to passcode, rather that if the former are not enrolled, then it will use the passcode.
// };
let fingerprintLableForOS = Platform.OS == "ios" ? "Touch ID" : "Fingerprint";
TouchID.authenticate('Login to [appname] using ' + fingerprintLableForOS)
.then(success => {
// console.log('Authenticated Successfully', success)
resolve(success)
})
.catch(error => {
console.log('Authentication Failed', error.code)
reject(error)
});
});
}
If authentication is successful then login to the server using saved credentials in keychain/Keystore.
let isFingerPrintAuthenticated = yield call(KeychainService.authenticateFingerPrint);
if (isFingerPrintAuthenticated === true) {
let credentials = yield call(KeychainService.getCredentials);
if (credentials && credentials.username) {
let savedPassword = JSON.parse(credentials.password),
savedUsername = credentials.username,
requestData = {};
requestData['user_name'] = savedUsername;
requestData['password'] = savedPassword.password;
}
// login with saved credentials
yield put({ type: "LOGIN_REQUESTED", requestData });
}
2. Login with credentials
Users can also cancel fingerprint popups and login via username and password. There can be two cases.
- The user whose credentials are already saved logs in
- Another user logs in
On credentials submitted, we’ll check if credentials match with the stored ones.
//check if saved username in keychain/keystore is same as data then continue
//else reset credentials
//or update credentials using setCredentials after successful login if password is //different from the saved one
let credentials = yield call(KeychainService.getCredentials);
if (credentials && credentials.username != user_name) {
yield call(KeychainService.resetCredentials);
}
//login to server
const loggedinUser = yield call(AuthService.login, action.requestData);
And add `resetCredentials` function in `keychainService.js`
export const resetCredentials = async () => {
return new Promise((resolve, reject) => {
Keychain.resetGenericPassword()
.then((response) => {
// console.log('response', response);
resolve(response);
})
.catch(err => {
console.log("err: ", err);
reject(err);
});
});
}