BLOG

Implementing a Biometrics Hook in React Native

Biometrics have become a very popular way to authenticate mobile users, and going forward, even desktop users. But does this mean Biometrics implementations have to be complicated and a science?

The image of Ben LokashBen Lokash
Jul 5, 2022
·
5 min read
technology
business
⭠ Back to postsBlog cover image

Biometrics is a must-have for any React Native app. Users expect to be able to authenticate with your app using all the methods offered by their device. There are several great npm libraries that make integrating biometrics into your React Native app easy. However, they only provide a thin wrapper around the native biometrics SDK. It is up to you to design an API that makes working with biometrics simple and maintainable.

I recently had a chance to refactor my project’s implementation of biometrics functionality. Before, we had logic scattered across the app, often using different implementations to achieve the same effect, such as prompting for a login, or clearing the storage. I was able to come up with a reusable hook design that I’m excited to share. Today we’re going to look at writing a hook that will encapsulate all functionality related to biometrics. In the end, you’ll have an easy-to-use hook to use in your components that looks like this:

// login screen
const { isBiometricsEnabled, promptBiometrics, biometricsType } = useBiometrics()

const handleBiometricsLogin = async () => {
	const { username, password } = await promptBiometrics()
	login(username, password)
}

return <Button onPress={handleBiometricsLogin}>
	Login with {biometricsType}
</Button>
// settings screen
const {
	enableBiometrics, disableBiometrics, isBiometricsEnabled,
	isBiometricsLoading, isBiometricsSupported } = useBiometrics()

const handleToggleBiometrics = () =>
	isBiometricsEnabled ? disableBiometrics() : enableBiometrics(username, password)

return (
	<Switch
    disabled={!isBiometricsSupported}
    value={isBiometricsEnabled}
    onValueChange={handleToggleBiometrics}
  />
)

Prerequisites

We’re going to be using React Context to make our biometrics hook available throughout the app. To interact with the native SDKs for biometrics and storage, we’re going to be using two libraries:

react-native-keychain
and
react-native-async-storage
. If you’re not already using these modules in your app, go ahead and check out the linked GitHub pages for install instructions.

We’re also going to assume you’re using Typescript, but you can still use these examples in a vanilla Javascript project - you’ll just have to remove the type definitions.

Creating the Context

We need to keep a few pieces of state around, like whether biometrics is enabled or currently loading. We’ll also need a way to access some singleton object that will contain our state (eg.

isBiometricsEnabled
) and our methods (eg.
enableBiometrics
). Sounds like a perfect use case for React Context! Let’s get started by defining the type for our context.

interface IBiometricsContext {
  isBiometricsSupported: boolean
  isBiometricsEnabled: boolean
  isBiometricsLoading: boolean
  biometricsType: Keychain.BIOMETRY_TYPE | null
  enableBiometrics: (username: string; password: string) => Promise<void>
  disableBiometrics: () => Promise<void>
  promptBiometrics: () => Promise<false | Keychain.UserCredentials>
}

Then we can instantiate the context object. Feel free to ignore type errors here. We are going to create another default values object that will override this one when we create the provider.

const BiometricsContext = createContext<IBiometricsContext>({
  isBiometricsSupported: false,
  isBiometricsEnabled: false,
  isBiometricsLoading: true,
  biometricsType: null,
  enableBiometrics: async () => undefined,
  disableBiometrics: async () => undefined,
  // @ts-ignore
  promptBiometrics: () => {}
})

Now we have our context object, but it’s not of much use to us until we create a provider to go along with it.

Creating the Provider

We’re now going to be implementing the logic behind our Provider. All of the following code snippets belong inside the body of our

BiometricsProvider
functional component:

export const BiometricsProvider: FC = ({ children }) => {
  // ...
}

If this is confusing, you may find it helpful to follow along with the file containing the end result, which you can find here.

Setting up our state

Since this is a regular react component, we can store our state in a simple

useState
.

const [isBiometricsSupported, setBiometricsSupported] = useState(false)
const [biometricsType, setBiometricsType] =
	useState<Keychain.BIOMETRY_TYPE | null>(null)
const [isBiometricsEnabled, setBiometricsEnabled] = useState(false)
const [isBiometricsLoading, setBiometricsLoading] = useState(true)

If this is confusing, you may find it helpful to follow along with the file containing the end result, which you can find here.

Notice the initial state of

isBiometricsLoading
is true. We need to do some initial setup when the provider mounts, after which we will set it to false.

Initialization

We are going to use our trusty

useEffect
with an empty dependency array to perform some initialization logic when the provider mounts.

useEffect(() => {
  const init = async () => {
    // 1
    const type = await Keychain.getSupportedBiometryType()
    if (!type) {
      setBiometricsLoading(false)
      return
    }
    // 2
    setBiometricsSupported(true)
    setBiometricsType(type)
    // 3
    setBiometricsEnabled((await AsyncStorage.getItem('is-biometrics-enabled')) === 'true')
    // 4
    setBiometricsLoading(false)
  }
  init()
}, [])


Let’s walk through this code. First off, we are not allowed to pass an

async
function to
useEffect
, so we create one inside the function and fire it at the end. This could also be an IIFE. Next, let’s check out the state updates:

  1. Get the type of biometrics supported by the device. If this returns null, biometrics is unsupported and we return early.

  2. If biometrics is supported, set

    isBiometricsSupported
    to true and store the type.

  3. Now we check the device storage to check if the user has previously enabled biometrics. This might seem strange, so let me explain. There exists a function to check which services we have stored credentials for:

    Keychain.getAllGenericPasswordServices()
    . However, it prompts the user for biometrics authentication, because the entries are encrypted. This doesn’t make sense for our scenario - we don’t want to prompt the user to authenticate when they haven’t asked for it. Instead, when the user enables biometrics, we will simply store a value in async storage (
    'is-biometrics-enabled’
    ) and check it during initialization. You’ll see this implementation shortly.

  4. Lastly, set

    isBiometricsLoading
    to false.

Core Logic

Let’s take a look at implementing the core functionality of our hook: enabling and disabling biometrics, and prompting the user for a biometrics authentication. First off, let’s tackle the enable function.

const enableBiometrics = async (username: string; password: string) => {
  // 1
  setBiometricsLoading(true)
  try {
    // 2
    await Keychain.setGenericPassword(username, password, {
      service: 'my-app',
      accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY,
      accessible: Keychain.ACCESSIBLE.WHEN_PASSCODE_SET_THIS_DEVICE_ONLY
     })
    // 3
    await AsyncStorage.setItem('is-biometrics-enabled', 'true')
    // 4
    setBiometricsEnabled(true)
    setBiometricsLoading(false)
  } catch (error) {
    // 5
    setBiometricsLoading(false)
    throw error
  }
}

Let’s walk through this line by line:

  1. Set

    isBiometricsLoading
    to true.

  2. Store the credentials, identifying our app as

    'my-app'
    . The
    accessControl
    option controls which authentication methods can be used to access these credentials. The
    accessible
    option can be used to disable credential access in certain scenarios, such as when the device is locked, or whether the credential should be shared across devices. Review the documentation for
    Keychain.ACCESS_CONTROL
    and
    Keychain.ACCESSIBLE
    enums and confirm that you are using the value that matches your use case.

  3. Next, we store our

    'is-biometrics-enabled'
    value in
    AsyncStorage
    , so on subsequent app launches, we can know if there are credentials stored on the device without prompting the user.

  4. If all goes well, set

    isBiometricsEnabled
    to true and
    isBiometricsLoading
    to false.

  5. Wrap it all in a try/catch block so we can set

    isBiometricsLoading
    to false in case of an error. Then, re-throw the error and let the caller deal with it 😉

Next, let’s take a look at the disable function. This one is pretty straightforward. We simply erase the credentials from Keychain and update our

AsyncStorage
value. We follow the same error handling strategy used in
enableBiometrics
.

const disableBiometrics = async () => {
  setBiometricsLoading(true)
  try {
    // erase the stored password
    await Keychain.resetGenericPassword({ service: 'my-app' })
    // update the async storage
    await AsyncStorage.setItem('is-biometrics-enabled', 'false')
    // update the local state
    setBiometricsEnabled(false)
    setBiometricsLoading(false)
  } catch (error) {
    setBiometricsLoading(false)
    throw error
  }
}

Finally, we are ready to implement our

promptBiometrics
function. This one simply wraps the
Keychain
call using the proper arguments.

const promptBiometrics = async () => {
  return Keychain.getGenericPassword({
    service: 'my-app',
    authenticationPrompt: {
      title: `Sign into MyApp using ${biometricsType}`
    }
  })
}

Whew! All that’s left to do is assemble everything into the object that will be made accessible by our provider, and return the JSX of the Provider itself.

return (
  <BiometricsContext.Provider
    value={{
      isBiometricsSupported,
      isBiometricsEnabled,
      isBiometricsLoading,
      biometricsType,
      enableBiometrics,
      disableBiometrics,
      promptBiometrics
    }}
  >
    {children}
  </BiometricsContext.Provider>
)

Let’s wrap up this with a bow and export our

useBiometrics
hook.

export default function useBiometrics() {
  return useContext(BiometricsContext)
}

Don’t forget to add the

BiometricsProvider
to your root
App
component as well.

// App.tsx
import { BiometricsProvider } from '~/providers/biometrics-provider'

export default function App() {
	<BiometricsProvider>
		// whatever else was here before...
	</BiometricsProvider>
}

That’s all folks! We have managed to fully encapsulate the logic and state of all biometrics functionality for our application. We can check if biometrics is enabled, prompt for a biometrics login and more, all by using the hook.

This also opens up opportunities for simplified testing - all you would need to do is write a mock hook that uses the same logic but leaves out the parts interacting with native SDKs!

We hope this post allows you to simplify your existing code, or write a new implementation with ease. Please check out the full code sample here. Happy hacking!

written by
Logo of QuantumMob

We are a Toronto-based end-to-end digital innovation firm with a passion for building beautiful & functional products that deliver results.

The image of hire us
You might also like