DEV Community

Allan López
Allan López

Posted on

Setting Up Biometric Login on Your React Native App

Introduction

Biometrics login is a convenient feature to have on your app, this convenience not only saves time but also improves user satisfaction and engagement. With biometric login, mobile app users can enjoy a seamless and secure authentication experience, making it a win-win for both security-conscious individuals and those seeking a hassle-free login solution.

On this post we are going to setup a biometric login feature that works appart from the normal email + password login.

Considerations

Check out the excellent Alto Engineering blog post about considerations for Implementing Biometric Login to learn about pros and cons of different ways to use a mobile device's biometrics to authenticate a user, with varying degrees of security.

On this post we are going to implement the Create a separate authentication mechanism for biometrics way on a expo RN app.

Note: We will only see the front-end implementation.

Setup

We will need to add some libraries to the project:

Flows

First Login

For the first login we are going to require strictly the email and password, the flow is as follows:

Login Flow

1. Login

The user must login with his credentials the first time in order to assure he is the owner of the account.

2. Show Biometrics

Ask for biometrics access so we can setup the biometrics login



import {
    authenticateAsync
} from 'expo-local-authentication';

const askForBiometrics = async () => 
    await authenticateAsync({
        disableDeviceFallback: true,
        promptMessage: "Enable biometrics login",
        cancelLabel: "Not now",
    });


Enter fullscreen mode Exit fullscreen mode

3. User confirms biometrics

4. Create a key pair

Create the public and private key, save the private key on the device in order to sign a payload, the public key will be use in the backend.



import {
    authenticateAsync
} from 'expo-local-authentication';
import {
    RSA
} from 'react-native-rsa-native';
...

const enableBiometrics = async () => {
    const biometricsResult = await askForBiometrics();
    if (biometricsResult?.success) {
        //Create Key pair
        const keys = await RSA.generateKeys(1024);
        // Use secure Store as it provides a way to encrypt and securely store key–value pairs locally on the device.
        await SecureStore.setItemAsync(
            "USER_BIOMETRIC_KEY",
            keys.private
        );
    }
};


Enter fullscreen mode Exit fullscreen mode

5. Send the public key

User public key will be saved on the backend in order to verify the signature.



...
const enableBiometrics = async () => {
    ...
    await postBiometricKey({
        biometricKey: {
            publicKey: keys.public
        }
    });
    ...
};



Enter fullscreen mode Exit fullscreen mode

Final Code



import {
    authenticateAsync
} from 'expo-local-authentication';
import {
    RSA
} from 'react-native-rsa-native';

const askForBiometrics = async () => 
    await authenticateAsync({
        disableDeviceFallback: true,
        promptMessage: "Enable biometrics login",
        cancelLabel: "Not now",
    });

const enableBiometrics = async () => {
    const biometricsResult = await askForBiometrics();
    if (biometricsResult?.success) {
        const keys = await RSA.generateKeys(1024);
        await SecureStore.setItemAsync(
            "USER_BIOMETRIC_KEY",
            keys.private
        );
        await postBiometricKey({
            biometricKey: {
                publicKey: keys.public
            }
        });
    }
};


Enter fullscreen mode Exit fullscreen mode

Next Logins

For next logins we will ask for biometrics if they have enabled it:

Biometrics flow

1. User Opens App

2. Show Biometrics

Ask for biometrics confirmation



import {
    authenticateAsync
} from 'expo-local-authentication';

const authenticate = async () => {
    const biometricsResult = await authenticateAsync({
        disableDeviceFallback: true,
        promptMessage: "Login with biometrics",
        cancelLabel: "Cancel",
    });
}


Enter fullscreen mode Exit fullscreen mode

3. User Confirms Biometrics

4. Creates a signature with private key

Sign a payload with the private key we saved on the user device using SecureStore.



import * as SecureStore from 'expo-secure-store';
import dayjs from 'dayjs';

const generateSignature = async () => {
    // Get private key from secure store
    const privateKey = await SecureStore.getItemAsync(
        "USER_BIOMETRIC_KEY"
    );
    const payload = {
        // You can add info you want on the payload
        // DO NOT SEND THE PRIVATE KEY
        // 5 minutes expiration
        exp: dayjs().add(5, 'minutes').unix(),
    };

    if (biometrics) {
        const signature = await sign(privateKey, payload);
        return signature;
    }
};


Enter fullscreen mode Exit fullscreen mode

Sign payload



import {
    RSA
} from 'react-native-rsa-native';

export async function sign(privateKey: string, payload: any) {
    const signature = await RSA.signWithAlgorithm(
        payload,
        privateKey,
        'SHA256withRSA'
    );
    return signature;
}


Enter fullscreen mode Exit fullscreen mode

5. Send signature to API



 const sendSignature = async () => {
    const signature = await generateSignature();
    if (signature) {
        // POST to API
        await postSignature({
            signature
        });
    }
};


Enter fullscreen mode Exit fullscreen mode

6. API validates signature

7. API returns a session token

Final Code



import {
    RSA
} from 'react-native-rsa-native';
import * as SecureStore from 'expo-secure-store';
import dayjs from 'dayjs';
import {
    authenticateAsync
} from 'expo-local-authentication';

const authenticate = async () => {
    const biometricsResult = await authenticateAsync({
        disableDeviceFallback: true,
        promptMessage: "Login with biometrics",
        cancelLabel: "Cancel",
    });
    if(biometricsResult?.success){
      await sendSignature();
    }
}

const sendSignature = async () => {
    const signature = await generateSignature();
    if (signature) {
        await postSignature({
            signature
        });
    }
};

const generateSignature = async () => {
    const biometrics = await SecureStore.getItemAsync(
        "USER_BIOMETRIC_KEY"
    );
    const payload = {
        exp: dayjs().add(5, 'minutes').unix(),
    };

    if (biometrics) {
        const signature = await sign(biometrics, payload);
        return signature;
    }
};


export async function sign(privateKey: string, payload: any) {
    const signature = await RSA.signWithAlgorithm(
        payload,
        privateKey,
        'SHA256withRSA'
    );
    return signature;
}



Enter fullscreen mode Exit fullscreen mode

Conclusion

And thats it! Now we have different flows isolated for login with email + password and biometrics.

Implementing face and fingerprint ID login offers evident advantages for both app users and the overall application. If you're contemplating integrating biometric login into your app, this blog post aims to provide you with valuable insights and guidance to navigate the process successfully.

Top comments (0)