capacitor-biometric-auth
This plugin for Capacitor 6 provides access to native biometry and device credentials on iOS and Android. It supports every type of biometry and every configuration option on both platforms. In addition, biometry and device credentials are simulated on the web so you can test your logic without making any changes to your code.
🛑 BREAKING CHANGES:
-
If you are upgrading from a version prior to 6.0.0, please note that authenticate()
now throws an instance of BiometryError
, and BiometryError.code
is now typed as BiometryErrorType
.
-
If you are upgrading from a version prior to 7.0.0, please note that authenticate()
will immediately present a prompt for device credentials if deviceIsSecure
is true, allowDeviceCredentials
is true, and no biometry of the requested strength is available.
-
If you are upgrading from a version prior to 8.0.0, please note that this plugin now requires Capacitor 6+.
-
If you are upgrading from a version prior to 8.0.0, please note that addResumeListener
now always returns a Promise and must be awaited.
Installation
pnpm add @aparajita/capacitor-biometric-auth
npm install @aparajita/capacitor-biometric-auth
yarn add @aparajita/capacitor-biometric-auth
Not using pnpm? You owe it to yourself to give it a try. It’s faster, better with monorepos, and uses way, way less disk space than the alternatives.
iOS
👉 IMPORTANT!! In order to use Face ID, you must add the NSFaceIDUsageDescription
key to your Info.plist
file. This is a string that describes why your app needs access to Face ID. If you don’t add this key, the system won’t allow your app to use Face ID.
- In Xcode, open your app’s
Info.plist
file. - Hover your mouse over one of the existing keys, and click the
+
button that appears. - In the popup that appears, type
Privacy - Face ID Usage Description
and press Enter. - In the Value column, enter a string that describes why your app needs access to Face ID.
- Save your changes.
Usage
The API is extensively documented in the TypeScript definitions file. There is also (somewhat incomplete auto-generated) documentation below. For a complete example of how to use this plugin in practice, see the demo app.
Checking availability
Although not strictly necessary, before giving the user the option to use biometry (such as displaying a biometry icon), you will probably want to call checkBiometry()
and inspect the CheckBiometryResult
to see what (if any) biometry and/or device credentials are available on the device. Note the following:
-
isAvailable
may be false
but biometryType
may indicate the presence of biometry on the device. This occurs if the current user is not enrolled in biometry, or if biometry has been disabled for the current app. In such cases the reason
and code
will tell you why.
-
On iOS, isAvailable
and strongBiometryIsAvailable
will always have the same value. On Android, isAvailable
will be true
if any type of biometry is available, but strongBiometryIsAvailable
will be true
only if strong biometry is available. For example, on a typical device, if the device supports both fingerprint and face authentication, isAvailable
will be true
if either is enrolled, but strongBiometryIsAvailable
will be true
only if fingerprint authentication is enrolled.
-
biometryTypes
may contain more than one type of biometry. This occurs on Android devices that support multiple types of biometry. In such cases biometryType
will indicate the primary (most secure) type of biometry, and the biometryTypes
array will contain all of the biometry types supported by the device. Note that Android only guarantees that one of the types is actually available.
Because the availability of biometry can change while your app is in the background, it’s important to check availability when your app resumes. By calling addResumeListener()
you can register a callback that is passed a CheckBiometryResult
when your app resumes.
Example
import { CheckBiometryResult } from './definitions'
let appListener: PluginListenerHandle
function updateBiometryInfo(info: CheckBiometryResult): void {
if (info.isAvailable) {
} else {
}
}
async function onComponentMounted(): Promise<void> {
updateBiometryInfo(await BiometricAuth.checkBiometry())
try {
appListener = await BiometricAuth.addResumeListener(updateBiometryInfo)
} catch (error) {
if (error instanceof Error) {
console.error(error.message)
}
}
}
async function onComponentUnmounted(): Promise<void> {
await appListener?.remove()
}
Authenticating
To initiate biometric authentication call authenticate()
. authenticate
takes an AuthenticateOptions
object which you will want to use in order to control the behavior and appearance of the biometric prompt.
If authentication succeeds, the Promise resolves. If authentication fails, the Promise is rejected with an instance of BiometryError
, which has two properties:
Property | Type | Description |
---|
message | string | A description of the error suitable for debugging |
code | BiometryErrorType | What caused the error |
Example
import {
AndroidBiometryStrength,
BiometryError,
BiometryErrorType,
} from './definitions'
async function authenticate(): Promise<void> {
try {
await BiometricAuth.authenticate({
reason: 'Please authenticate',
cancelTitle: 'Cancel',
allowDeviceCredential: true,
iosFallbackTitle: 'Use passcode',
androidTitle: 'Biometric login',
androidSubtitle: 'Log in using biometric authentication',
androidConfirmationRequired: false,
androidBiometryStrength: AndroidBiometryStrength.weak,
})
} catch (error) {
if (error instanceof BiometryError) {
if (error.code !== BiometryErrorType.userCancel) {
await showAlert(error.message)
}
}
}
}
Biometry support
web
On the web, you can fake any of the supported biometry types by calling setBiometryType()
.
iOS
On iOS, Touch ID and Face ID are supported.
Android
On Android, fingerprint, face, and iris authentication are supported. Note that if a device supports more than one type of biometry, the plugin will initially present the primary (most secure) available type, which is determined by the system.
API
This is the public interface of the plugin.
checkBiometry()
checkBiometry() => Promise<CheckBiometryResult>
Check to see what biometry type (if any) is available.
Returns: Promise<CheckBiometryResult>
setBiometryType(...)
setBiometryType(type: BiometryType | string | Array<BiometryType | string> | undefined) => Promise<void>
web only
On the web, this method allows you to dynamically simulate different types of biometry. You may either pass BiometryType
enums or the string names of the BiometryType
enums. If undefined or a string is passed and it isn't a valid value, nothing happens.
setBiometryIsEnrolled(...)
setBiometryIsEnrolled(isSecure: boolean) => Promise<void>
web only
On the web, this method allows you to dynamically simulate whether or not the user has enrolled in biometry.
setDeviceIsSecure(...)
setDeviceIsSecure(isSecure: boolean) => Promise<void>
web only
On the web, this method allows you to dynamically simulate whether or not the user has secured the device with a PIN, pattern or passcode.
authenticate(...)
authenticate(options?: AuthenticateOptions) => Promise<void>
Prompt the user for authentication. If authorization fails for any reason, the promise is rejected with a BiometryError
.
For detailed information about the behavior on iOS, see:
https://developer.apple.com/documentation/localauthentication/lapolicy/deviceownerauthenticationwithbiometrics
Some versions of Android impose a limit on the number of failed attempts. If allowDeviceCredential
is true
, when the limit is reached the user will then be presented with a device credential prompt. If allowDeviceCredential
is false
, when the limit is reached authenticate()
will reject with a BiometryErrorType
of biometryLockout
, after which the user will have to wait the system-defined length of time before being allowed to authenticate again.
addResumeListener(...)
addResumeListener(listener: ResumeListener) => Promise<PluginListenerHandle>
Register a function that will be called when the app resumes. The function will be passed the result of checkBiometry()
.
👉 NOTE: checkBiometry() must be called at least once before calling this method.
Returns: Promise<PluginListenerHandle>
Interfaces
CheckBiometryResult
Prop | Type | Description |
---|
isAvailable | boolean | True if the device supports at least weak biometric authentication and the current user has enrolled in some form of biometry. Note that if strongBiometryIsAvailable is true, this will also be true. |
strongBiometryIsAvailable | boolean | True if the device has strong biometric authentication capability and the current user has enrolled in that strong biometry.
On iOS this value and isAvailable will always be the same, since iOS only supports strong biometry.
On Android, for example, if the device supports both fingerprint and face authentication, and the user has enrolled only in face authentication, and Android considers face authentication on that device to be weak, then isAvailable will be true but this value will be false. |
biometryType | BiometryType | The primary (most secure) type of biometry supported by the device. Note that supported is not the same as available, which requires the biometry to be enrolled. |
biometryTypes | BiometryType[] | All of the biometry types supported by the hardware on the device (currently only Android devices support multiple biometry types). If no biometry is supported, i.e. biometryType === <a href="#biometrytype">BiometryType.none , this will be an empty array.
Note that supported is not the same as available, which requires the biometry to be enrolled. |
deviceIsSecure | boolean | Returns true if the device is secure. On iOS, this means that the device has a passcode set. On Android, this means that the device has a PIN, pattern, or password set. |
reason | string | If weak or better biometry is not available and the system gives a reason why, it will be returned here. Otherwise it's an empty string. |
code | BiometryErrorType | If weak or better biometry is not available, the error code will be returned here. Otherwise it's an empty string. The error code will be one of the BiometryErrorType enum values, and is consistent across platforms. |
strongReason | string | If strong biometry is not available and the system gives a reason why, it will be returned here. Otherwise it's an empty string.
On iOS, this will always be the same as reason , since all biometry on iOS is strong. |
strongCode | BiometryErrorType | If strong biometry is not available, the error code will be returned here. Otherwise it's an empty string. The error code will be one of the BiometryErrorType enum values, and is consistent across platforms.
On iOS, this will always be the same as code , since all biometry on iOS is strong. |
Array
Prop | Type | Description |
---|
length | number | Gets or sets the length of the array. This is a number one higher than the highest index in the array. |
Method | Signature | Description |
---|
toString | () => string | Returns a string representation of an array. |
toLocaleString | () => string | Returns a string representation of an array. The elements are converted to string using their toLocaleString methods. |
pop | () => T | undefined | Removes the last element from an array and returns it. |
If the array is empty, undefined is returned and the array is not modified. | | |
push | (...items: T[]) => number | Appends new elements to the end of an array, and returns the new length of the array. |
concat | (...items: ConcatArray<T>[]) => T[] | Combines two or more arrays. |
This method returns a new array without modifying any existing arrays. | | |
concat | (...items: (T | ConcatArray<T>)[]) => T[] | Combines two or more arrays. |
This method returns a new array without modifying any existing arrays. | | |
join | (separator?: string) => string | Adds all the elements of an array into a string, separated by the specified separator string. |
reverse | () => T[] | Reverses the elements in an array in place. |
This method mutates the array and returns a reference to the same array. | | |
shift | () => T | undefined | Removes the first element from an array and returns it. |
If the array is empty, undefined is returned and the array is not modified. | | |
slice | (start?: number, end?: number) => T[] | Returns a copy of a section of an array. |
For both start and end, a negative index can be used to indicate an offset from the end of the array.
For example, -2 refers to the second to last element of the array. |
| sort | (compareFn?: ((a: T, b: T) => number) | undefined) => this | Sorts an array in place.
This method mutates the array and returns a reference to the same array. |
| splice | (start: number, deleteCount?: number) => T[] | Removes elements from an array and, if necessary, inserts new elements in their place, returning the deleted elements. |
| splice | (start: number, deleteCount: number, ...items: T[]) => T[] | Removes elements from an array and, if necessary, inserts new elements in their place, returning the deleted elements. |
| unshift | (...items: T[]) => number | Inserts new elements at the start of an array, and returns the new length of the array. |
| indexOf | (searchElement: T, fromIndex?: number) => number | Returns the index of the first occurrence of a value in an array, or -1 if it is not present. |
| lastIndexOf | (searchElement: T, fromIndex?: number) => number | Returns the index of the last occurrence of a specified value in an array, or -1 if it is not present. |
| every | <S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any) => this is S[] | Determines whether all the members of an array satisfy the specified test. |
| every | (predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any) => boolean | Determines whether all the members of an array satisfy the specified test. |
| some | (predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any) => boolean | Determines whether the specified callback function returns true for any element of an array. |
| forEach | (callbackfn: (value: T, index: number, array: T[]) => void, thisArg?: any) => void | Performs the specified action for each element in an array. |
| map | <U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any) => U[] | Calls a defined callback function on each element of an array, and returns an array that contains the results. |
| filter | <S extends T>(predicate: (value: T, index: number, array: T[]) => value is S, thisArg?: any) => S[] | Returns the elements of an array that meet the condition specified in a callback function. |
| filter | (predicate: (value: T, index: number, array: T[]) => unknown, thisArg?: any) => T[] | Returns the elements of an array that meet the condition specified in a callback function. |
| reduce | (callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T) => T | Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function. |
| reduce | (callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T) => T | |
| reduce | <U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U) => U | Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function. |
| reduceRight | (callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T) => T | Calls the specified callback function for all the elements in an array, in descending order. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function. |
| reduceRight | (callbackfn: (previousValue: T, currentValue: T, currentIndex: number, array: T[]) => T, initialValue: T) => T | |
| reduceRight | <U>(callbackfn: (previousValue: U, currentValue: T, currentIndex: number, array: T[]) => U, initialValue: U) => U | Calls the specified callback function for all the elements in an array, in descending order. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function. |
ConcatArray
Method | Signature |
---|
join | (separator?: string) => string |
slice | (start?: number, end?: number) => T[] |
AuthenticateOptions
Prop | Type | Description |
---|
reason | string | Displays the reason for requesting authentication in the authentication dialog presented to the user.
Default: System default |
cancelTitle | string | iOS: The system presents a cancel button during biometric authentication to let the user abort the authentication attempt. The button appears every time the system asks the user to present a finger registered with Touch ID. For Face ID, the button only appears if authentication fails and the user is prompted to try again. Either way, the user can stop trying to authenticate by tapping the button.
Android: The text for the negative button. This would typically be used as a "Cancel" button, but may be also used to show an alternative method for authentication, such as a screen that asks for a backup password.
Default: "Cancel" |
allowDeviceCredential | boolean | If true, allows for authentication using device unlock credentials.
Default: false.
iOS: If biometry is available, enrolled, and not disabled, the system uses that first. After the first Touch ID failure or second Face ID failure, if iosFallbackTitle is not an empty string, a fallback button appears in the authentication dialog. If the user taps the fallback button, the system prompts the user for the device passcode or the user’s password. If iosFallbackTitle is an empty string, no fallback button will appear.
If no biometry is enrolled and enabled, and a passcode is set, the system immediately prompts the user for the device passcode or user’s password. Authentication fails with the error code passcodeNotSet if the device passcode isn’t enabled.
If a passcode is not set on the device, a passcodeNotSet error is returned.
The system disables passcode authentication after 6 unsuccessful attempts, with progressively increasing delays between attempts.
The title of the fallback button may be customized by setting iosFallbackTitle to a non-empty string.
Android: The user will first be prompted to authenticate with biometrics, but also given the option to authenticate with their device PIN, pattern, or password by tapping a button in the authentication dialog. The title of the button is supplied by the system. |
iosFallbackTitle | string | The system presents a fallback button when biometric authentication fails — for example, because the system doesn’t recognize the presented finger, or after several failed attempts to recognize the user’s face.
If allowDeviceCredential is false, tapping this button dismisses the authentication dialog and returns the error code userFallback. If undefined, the localized system default title is used. Passing an empty string hides the fallback button completely.
If allowDeviceCredential is true and this is undefined, the localized system default title is used. |
androidTitle | string | Title for the Android dialog. If not supplied, the system default is used. |
androidSubtitle | string | Subtitle for the Android dialog. If not supplied, the system default is used. |
androidConfirmationRequired | boolean | Determines if successful weak biometric authentication must be confirmed.
For information on this setting, see https://developer.android.com/reference/android/hardware/biometrics/BiometricPrompt.Builder#setConfirmationRequired(boolean).
Default: true |
androidBiometryStrength | AndroidBiometryStrength | Set the strength of Android biometric authentication that will be accepted.
👉 NOTE: On Android 9 & 10 (API 28-29), this will effectively always be .weak if allowDeviceCredential is true. This is a known limitation of the Android API. 🤯
Default: AndroidBiometryStrength.weak |
PluginListenerHandle
Method | Signature |
---|
remove | () => Promise<void> |
Type Aliases
ResumeListener
The signature of the callback passed to addResumeListener()
.
(info: CheckBiometryResult): void
Enums
BiometryType
Members | Description |
---|
none | |
touchId | iOS Touch ID |
faceId | iOS Face ID |
fingerprintAuthentication | Android fingerprint authentication |
faceAuthentication | Android face authentication |
irisAuthentication | Android iris authentication |
BiometryErrorType
Members | Value |
---|
none | '' |
appCancel | 'appCancel' |
authenticationFailed | 'authenticationFailed' |
invalidContext | 'invalidContext' |
notInteractive | 'notInteractive' |
passcodeNotSet | 'passcodeNotSet' |
systemCancel | 'systemCancel' |
userCancel | 'userCancel' |
userFallback | 'userFallback' |
biometryLockout | 'biometryLockout' |
biometryNotAvailable | 'biometryNotAvailable' |
biometryNotEnrolled | 'biometryNotEnrolled' |
noDeviceCredential | 'noDeviceCredential' |
AndroidBiometryStrength
Members | Description |
---|
weak | authenticate() will present any available biometry. |
strong | authenticate() will only present strong biometry. |