Build a custom authentication flow using biometric sign-in
Biometric sign-in lets returning users sign in with their device's biometrics. After a normal sign-in or sign-up, your app can enroll the current installation as a biometric credential. When the session expires, the user can sign in again with biometrics instead of their original sign-in method.
This is different from using biometrics only to unlock local app content. Biometric sign-in authenticates with Clerk and creates a Clerk session. It is also different from a passkey: biometric credentials are scoped to the native app installation and don't use WebAuthn.
During enrollment, the SDK generates a device-bound key pair and registers the public key with Clerk. The private key never leaves the device. To sign in, the device signs a one-time Clerk challenge after the user approves the system authentication prompt. Clerk verifies the signature before creating a session.
Enable biometric sign-in
- In the Clerk Dashboard, navigate to the Native applications page and enable the Native API. This is required to integrate Clerk in your native application or browser extension.
- Navigate to the User & Authentication page and select the Biometric tab.
- Enable Sign-in with mobile biometrics.
If you use Clerk's prebuilt authentication views, you can also enable Prompt after sign-in or Prompt after sign-up. The prebuilt views will then offer enrollment after authentication and display biometric sign-in when a signed-out user has an enrolled credential. For a custom flow, use the APIs demonstrated in this guide.
Install @clerk/expo-biometrics, then rebuild your development build:
npx expo install @clerk/expo-biometricsTo support Face ID, set the faceIDPermission option on the @clerk/expo config plugin. It adds NSFaceIDUsageDescription to your app's Info.plist. The value must explain why your app uses Face ID.
{
"expo": {
"plugins": [["@clerk/expo", { "faceIDPermission": "Use Face ID to sign in to this app." }]]
}
}Biometric sign-in is supported on Android 9 (API level 28) and later. The device must have a Class 3 biometric enrolled. Clerk automatically adds the USE_BIOMETRIC permission to your app's merged manifest.
Enroll a biometric credential
Enroll the current app installation only after the user completes a normal sign-in or sign-up and Clerk has a session with a status of active or pending.
The following example demonstrates how to enroll the current Expo app installation. The reason is displayed in the system authentication prompt.
import { useBiometricCredentials } from '@clerk/expo/biometrics'
import { Button } from 'react-native'
export function EnrollBiometricCredential() {
const { enroll } = useBiometricCredentials()
const enrollCurrentDevice = async () => {
try {
await enroll({ reason: 'Use biometrics to sign in.' })
} catch (err) {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling.
console.error(JSON.stringify(err, null, 2))
}
}
return <Button title="Enable biometric sign-in" onPress={enrollCurrentDevice} />
}Sign a returning user in
When the user no longer has an active session, check whether a local biometric credential is available before displaying biometric sign-in. While signed out, this availability check only inspects local state and can't confirm that the credential still exists in Clerk. If the subsequent sign-in reports that the credential no longer exists, the SDK removes the stale local state.
The following example checks availability, signs the user in, and sets the created session as active.
import { useEffect, useState } from 'react'
import { useAuth } from '@clerk/expo'
import { useBiometricCredentials } from '@clerk/expo/biometrics'
import { Button } from 'react-native'
export function SignInWithBiometrics() {
const { isLoaded } = useAuth()
const { getAvailability, signIn } = useBiometricCredentials()
const [isAvailable, setIsAvailable] = useState(false)
const refreshAvailability = async () => {
try {
const availability = await getAvailability()
setIsAvailable(availability.isAvailable)
} catch {
setIsAvailable(false)
}
}
useEffect(() => {
if (isLoaded) void refreshAvailability()
}, [isLoaded])
const handleSignIn = async () => {
try {
const result = await signIn()
if (result.status === 'complete' && result.createdSessionId) {
await result.setActive({ session: result.createdSessionId })
}
} catch (err) {
// Keep another sign-in method available when biometric sign-in fails.
console.error(JSON.stringify(err, null, 2))
await refreshAvailability()
}
}
if (!isLoaded || !isAvailable) return null
return <Button title="Sign in with biometrics" onPress={handleSignIn} />
}Reverify an active session
Reverification prompts a signed-in user to verify their identity again before a sensitive action. While the user is signed in, reverify their identity with an enrolled biometric credential. This requires an active session and a credential enrolled with 'biometry_current_set'. Successful reverification updates the existing session and refreshes its token, so you don't need to call setActive().
Wrap the request to your backend with the useReverification() hook. When your backend responds that the action requires reverification, the hook calls onNeedsReverification and retries the request after you call complete(). Expo has no prebuilt reverification UI, so onNeedsReverification is required.
The following example sends a request to a route that requires reverification and calls reverify() when the route asks for it. If the biometric verification doesn't complete, the example calls cancel(), which rejects the request. Replace the route and the console messages with your own.
import { useAuth, useReverification } from '@clerk/expo'
import { isBiometricCredentialError, useBiometricCredentials } from '@clerk/expo/biometrics'
import { Button } from 'react-native'
export function TransferButton() {
const { getToken } = useAuth()
const { reverify } = useBiometricCredentials()
const transferMoney = useReverification(
async () => {
const token = await getToken()
const res = await fetch('https://example.com/api/reverification-example', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
})
return res.json()
},
{
onNeedsReverification: async ({ level, complete, cancel }) => {
try {
const result = await reverify({
level,
reason: 'Verify your identity to continue.',
})
if (result.status !== 'complete') {
console.error(
'Biometric verification is incomplete. Offer another verification method.',
)
cancel()
return
}
complete()
} catch (err) {
if (
isBiometricCredentialError(err) &&
err.code === 'biometric_credential_policy_incompatible'
) {
console.error('Choose another verification method for this credential.')
}
cancel()
}
},
},
)
const handlePress = async () => {
try {
await transferMoney()
} catch (err) {
console.error(JSON.stringify(err, null, 2))
}
}
return <Button title="Transfer" onPress={handlePress} />
}Revoke a biometric credential
While the user is signed in, list their active biometric credentials and let them choose which credential to revoke. Revoking a credential prevents Clerk from accepting it again. When the matching credential belongs to the current app installation, the SDK also deletes its local private key and credential metadata.
The following example lists biometric credentials and revokes the selected credential.
import { useEffect, useState } from 'react'
import { type BiometricCredential, useBiometricCredentials } from '@clerk/expo/biometrics'
import { Button, Text, View } from 'react-native'
export function RevokeBiometricCredential() {
const { list, revoke } = useBiometricCredentials()
const [credentials, setCredentials] = useState<BiometricCredential[]>([])
const loadBiometricCredentials = async () => {
try {
setCredentials(await list())
} catch (err) {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling.
console.error(JSON.stringify(err, null, 2))
}
}
const handleRevoke = async (id: string) => {
try {
await revoke(id)
setCredentials((currentCredentials) =>
currentCredentials.filter((credential) => credential.id !== id),
)
} catch (err) {
// See https://clerk.com/docs/guides/development/custom-flows/error-handling
// for more info on error handling.
console.error(JSON.stringify(err, null, 2))
}
}
useEffect(() => {
void loadBiometricCredentials()
}, [])
return (
<View>
{credentials.map((credential) => (
<View key={credential.id}>
<Text>{credential.name ?? 'Biometric credential'}</Text>
<Button title="Revoke" onPress={() => handleRevoke(credential.id)} />
</View>
))}
</View>
)
}Feedback
Last updated on