Native Passkeys on iOS 15.0+ and Android API 28+ using React Native.
You can find an example backend for testing here.
For the javascript part of the installation you need to run
npm install react-native-passkeyor
yarn add react-native-passkeyFor the native part of the installation you need to run
cd ios && pod installin the root of your React Native project.
There are iOS specific steps you need to go through in order to configure Passkey support. If you have already set up an associated domain for your application you can skip this step.
Set up an associated domain for your application (More info)
-
You need to associate a domain with your application. On your webserver set up this route:
GET https://<yourdomain>/.well-known/apple-app-site-association -
This route should serve a static JSON object containing your team id and bundle identifier. Example (replace XXXXXXXXXX with your team identifier and the rest with your bundle id, e.g. "H123456789.com.mtrx0.passkeyExample"):
{ "applinks": {}, "webcredentials": { "apps": ["XXXXXXXXXX.YYY.YYYYY.YYYYYYYYYYYYYY"] }, "appclips": {} } -
In XCode under
Signing & Capabilitiesadd a new Capability of typeAssociated Domains. Now add this and replace XXXXXX with your domain (e.g.apple.com)webcredentials:XXXXXX
The Android specific configuration is similar to iOS. If you have already set up Digital Asset Links for your application you can skip this step.
Associate your app with a domain (More info)
-
You need to associate a domain with your application. On your webserver set up this route:
GET https://<yourdomain>/.well-known/assetlinks.json -
This route should serve a static JSON object containing the following information. Example (replace with your data, replace SHA_HEX_VALUE with the SHA256 fingerprints of your Android signing certificate)
[{ "relation": ["delegate_permission/common.get_login_creds"], "target": { "namespace": "android_app", "package_name": "com.example", "sha256_cert_fingerprints": [ SHA_HEX_VALUE ] } }]
If you are having issues with your backend setup you can look at an example here.
import { Passkey } from 'react-native-passkey';
// Use this method to check if passkeys are supported on the device
const isSupported: boolean = Passkey.isSupported();import { Passkey, PasskeyCreateResult } from 'react-native-passkey';
// Retrieve a valid FIDO2 attestation request from your server
// The challenge inside the request needs to be a base64URL encoded string
// There are plenty of libraries which can be used for this (e.g. fido2-lib)
try {
// Call the `create` method with the retrieved request in JSON format
// A native overlay will be displayed
const result: PasskeyCreateResult = await Passkey.create(requestJson);
// The `create` method returns a FIDO2 attestation result
// Pass it to your server for verification
} catch (error) {
// Handle Error...
}import { Passkey, PasskeyGetResult } from 'react-native-passkey';
// Retrieve a valid FIDO2 assertion request from your server
// The challenge inside the request needs to be a base64URL encoded string
// There are plenty of libraries which can be used for this (e.g. fido2-lib)
try {
// Call the `get` method with the retrieved request in JSON format
// A native overlay will be displayed
const result: PasskeyGetResult = await Passkey.get(requestJson);
// The `get` method returns a FIDO2 assertion result
// Pass it to your server for verification
} catch (error) {
// Handle Error...
}Use Passkey.getImmediate() to authenticate only when a credential is
already available on the device, without surfacing the system modal when
nothing matches. This is useful for opportunistic sign-in checks (e.g. on app
launch or on a sign-in screen) where you do not want to interrupt the user
with a credential picker if no passkey exists.
- iOS 16+: uses
ASAuthorizationController.preferImmediatelyAvailableCredentials - Android: uses the
preferImmediatelyAvailableCredentialsflag onGetCredentialRequest(Credential Manager) - iOS < 16: falls back to a normal
get()request (no silent behaviour available on the platform)
When no credential is available the call rejects with a NoCredentials error
and no UI is shown. Handle this error to fall back to your usual sign-in flow.
If a credential was available and the user dismissed the sheet, the call
rejects with UserCancelled instead. The two are distinct on both platforms, so
UserCancelled from getImmediate() is a reliable signal that the device holds
a passkey for your relying party — useful for deciding whether to offer a
"Sign in with passkey" retry after a dismissal.
import { Passkey } from 'react-native-passkey';
try {
const result = await Passkey.getImmediate(requestJson);
// Credential available — pass to your server
} catch (error) {
if (error.error === 'NoCredentials') {
// No passkey on device — fall back to password / OTP / etc.
} else if (error.error === 'UserCancelled') {
// User dismissed the sheet
} else {
// Handle other errors
}
}The WebAuthn Signal API lets your app keep OS credential managers (e.g. iCloud Keychain, Google Password Manager) in sync with your server. When the server has revoked or deleted a passkey, signalling it removes / hides the stale credential so it no longer shows up in the system sheet.
Both methods are best-effort: they resolve once the request is accepted and no-op on OS versions without Signal API support (iOS < 26). All credential ids and the user handle are passed as Base64URL encoded strings.
- iOS 26+: uses
ASCredentialUpdater - Android: uses
CredentialManager.signalCredentialState(requiresandroidx.credentials1.6.0+, bundled with the library)
Reports a single credential the relying party no longer recognizes. Use this when unauthenticated (e.g. after a failed sign-in): it takes a single credential id and no user handle, so it reveals nothing about the user.
import { Passkey } from 'react-native-passkey';
// e.g. after the server rejects the credential used to authenticate
await Passkey.signalUnknownCredential({
rpId: 'example.com',
credentialId, // Base64URL encoded credential id
});Reports the complete set of credential ids the relying party still accepts for a user. OS credential managers remove / hide any stored credentials not in the list (reversible — re-signal an id to restore it; an empty list hides all). Use this when authenticated (after login, or after adding / deleting a passkey): it needs the user handle and the full accepted set, so it authoritatively prunes.
import { Passkey } from 'react-native-passkey';
// e.g. after deleting a passkey, signal the remaining accepted credentials
await Passkey.signalAllAcceptedCredentials({
rpId: 'example.com',
userId, // Base64URL encoded WebAuthn user handle
allAcceptedCredentialIds, // string[] of Base64URL encoded credential ids
});The library normalises native error codes into the following set:
error value |
Meaning |
|---|---|
NotSupported |
Passkeys are not supported on this device / OS version |
RequestFailed |
Generic request failure (e.g. transport / network / invalid response) |
UserCancelled |
User dismissed the system sheet |
InvalidChallenge |
The provided challenge could not be decoded |
InvalidUserId |
The provided user id could not be decoded (registration only) |
BadConfiguration |
App is not configured correctly (associated domain / asset links) |
NoCredentials |
No credential is available — also returned for silent getImmediate() |
CredentialAlreadyExists |
A passkey already exists for this account on this device (registration) |
NoCreateOption |
No credential provider can create a passkey (Android; e.g. no Google account signed in) |
Interrupted |
The operation was interrupted and may be retried |
TimedOut |
The operation timed out |
UnknownError |
Unknown / unmapped error |
You can force users to register and authenticate using either a platform key, a security key (like Yubikey) or allow both using the following methods. This only works on iOS, Android will ignore these instructions.
Passkey.create()- Allow the user to choose between platform and security passkeyPasskey.createPlatformKey()- Force the user to create a platform passkeyPasskey.createSecurityKey()- Force the user to create a security passkey
Passkey.get()- Allow the user to choose between platform and security passkeyPasskey.getPlatformKey()- Force the user to authenticate using a platform passkeyPasskey.getSecurityKey()- Force the user to authenticate using a security passkey
As of version 3.0 the largeBlob extension will work on iOS 17+ only.
You can use the largeBlob extension to store a small amount of opaque data associated with the stored passkey.
During registration you can check whether the selected authenticator supports the largeBlob extension. Pass 'preferred' to allow registration to proceed even if the authenticator does not support it, or 'required' to fail if it does not.
// Request
{
...
extensions: {
largeBlob: {
support: 'preferred' | 'required'
}
}
}
// Response
{
...
clientExtensionResults: {
largeBlob: {
supported: boolean
}
}
}If the largeBlob extension is supported you can write data to it during the assertion process. This does not work during registration.
// Request
{
...
extensions: {
largeBlob: {
write: Uint8Array
}
}
}
// Response
{
...
clientExtensionResults: {
largeBlob: {
written: true
}
}
}After writing you can read the data on any following assertion.
// Request
{
...
extensions: {
largeBlob: {
read: true
}
}
}
// Response
{
...
clientExtensionResults: {
largeBlob: {
blob: number[] // convert to Uint8Array if needed: new Uint8Array(blob)
}
}
}You can find information on the largeBlob extension in the WebAuthn specification here.
As of version 3.3 the PRF extension will work for Android and iOS 18+.
On Android, binary extension inputs are sent to Credential Manager as standard WebAuthn JSON. You can pass PRF salts as a Uint8Array, ArrayBuffer, number[], or a Base64URL string; the library normalizes them to Base64URL before calling the native Android API. This avoids Android rejecting otherwise valid PRF creation requests with No create options available.
You can use the PRF extension to retrieve a secret which allows for various use cases like encryption of user data.
During registration you can either pass in an empty object (this will check for PRF support) or a salt (with an optional second) to retrieve the secret.
// Request
{
...
extensions: {
prf: {}
}
}
// Response
{
...
clientExtensionResults: {
prf: {
enabled: boolean
results: {}
}
}
}You can do this either when creating or when asserting the passkey.
// Request
{
...
extensions: {
prf: {
eval: {
first: Uint8Array | ArrayBuffer | number[] | string
second: Uint8Array | ArrayBuffer | number[] | string // optional
}
}
}
}
// Response
{
...
clientExtensionResults: {
prf: {
enabled: true
results: {
first: 'Be3rf7AK8fwisd9vO13uqaP92XA24jKMSUaEaMclWIk=',
second: 'jbVCsIGJvtSWv6LRG3fHpUaG/BvT75b8ZLRAuLBNUpk='
},
}
}
}You can also use evalByCredential to retrieve secrets for specific credentials. You can read more in the WebAuthn specification here.
See the contributing guide to learn how to contribute to the repository and the development workflow.
MIT