Binding keys to a device in React Native
PingOne Advanced Identity Cloud PingAM React Native
The Device Binding module provides secure device registration and authentication capabilities for React Native applications. It enables applications to bind cryptographic keys to a device and restrict access to those keys using biometrics, a PIN, or other additional authentication methods.
Before you begin
You need to create an authentication journey in your server using the appropriate nodes to enable device binding.
The nodes you can use for device binding Journeys include the follows:
- Device Binding node
-
Allows users to register one or more devices to their account. A user can bind multiple devices, and each device can be bound to multiple users.
The client receives a
DeviceBindingCallbackwhen reaching this node in a journey. - Device Signing Verifier node
-
Verifies possession of a registered bound device.
The node requires the client device to sign a challenge string using the private key that corresponds to the public key stored on the server during initial binding.
The client receives a
DeviceSigningVerifierCallbackwhen reaching this node in a journey. - Device Binding Storage node
-
Optionally persists collected device binding data to a user’s profile in the identity store.
By default, the Device Binding node stores device data in the user’s profile. You can choose instead to store the device data in transient state, perhaps to run a custom script to extract additional context.
In this case, you can use a Device Binding Storage node to store the data in the user’s profile.
This node runs entirely server-side, and doesn’t send a callback to the client.
Securing access to the keys
The Device Binding module supports four distinct methods for accessing the private key, each offering different levels of security and user experience.
You specify which authentication type your client uses in the configuration of the Device Binding node. To change the authentication type to access the keys, you’ll need to rebind the client device
-
Biometric Only
-
Biometric with Fallback
-
Application PIN
-
No Authentication
| Type name |
|
| Description |
Requires strict biometric authentication with no fallback options |
| Security level |
High |
| User experience |
Streamlined for devices with reliable biometric sensors |
| Behavior |
|
| Use cases |
Financial applications, enterprise security, medical applications |
| Device requirements |
Must have functional biometric sensors and enrolled biometric data |
| Type name |
|
| Description |
Prefers biometric authentication but allows fallback to device credentials |
| Security level |
Medium to High |
| User experience |
Flexible with multiple authentication options |
| Behavior |
|
| Use cases |
Consumer applications, general-purpose authentication, accessibility-focused apps |
| Device requirements |
|
| Type name |
|
| Description |
Requires a custom PIN that the application manages entirely |
| Security level |
Medium |
| User experience |
Consistent across all devices regardless of hardware capabilities |
| Behavior |
|
| Use cases |
|
| Device requirements |
None - works on all devices |
| Type name |
|
| Description |
No user authentication required to access cryptographic keys |
| Security Level |
Low |
| User Experience |
Seamless with no authentication prompts |
| Behavior |
|
| Use cases |
|
| Security Considerations |
Anyone with device access can use the cryptographic keys |
| Device Requirements |
None |
Installing modules
To install the module into your React Native project, use yarn or npm as follows:
-
yarn
-
npm
yarn add @ping-identity/rn-binding
npm install @ping-identity/rn-binding
For Journey-integrated collection, also install the Journey module if you have not already done so:
-
yarn
-
npm
yarn add @ping-identity/rn-journey
npm install @ping-identity/rn-journey
Create the binding client
You create a binding client by calling createBindingClient(), which creates the client once at app startup or when configuring your journey integrations. You can reuse it for the lifetime of the app:
import { createBindingClient } from '@ping-identity/rn-binding';
import { logger } from '@ping-identity/rn-logger';
const bindingLogger = logger({ level: 'debug' });
const bindingClient = createBindingClient({
logger: bindingLogger,
});
Configuration options
| Option | Required | Description |
|---|---|---|
|
No |
A Provides diagnostic output for binding operations. Learn more in Configuring logging in React Native. |
|
No |
A function Replaces the default native PIN dialog. See Providing a custom PIN collector. |
|
No |
A function Replaces the default native key selector. See Providing a custom key selector. |
|
No |
A storage handle for persisting bound key metadata. Useful for sharing key state across app sessions. |
Binding keys to a device
When a journey node contains a Device Binding step, the journey callback type is DeviceBindingCallback.
Call bindingClient.bindForJourney() to generate a cryptographic key pair, authenticate the user, and register the public key with the server:
import { createBindingClient } from '@ping-identity/rn-binding';
import { useJourney } from '@ping-identity/rn-journey';
const bindingClient = createBindingClient();
const [node, actions] = useJourney(journeyClient);
async function handleDeviceBinding(deviceName?: string) {
await bindingClient.bindForJourney(journeyClient, {
deviceName, // Optional: custom label for this device
});
// The callback has been fulfilled — advance the journey
await actions.next();
}
The binding process performs the following steps:
-
Validates device support for the requested authentication type.
-
Removes any existing keys for the user on this device.
-
Generates a new cryptographic key pair.
-
Authenticates the user (biometric, PIN, or none, depending on server configuration).
-
Creates a signed proof-of-possession JWT.
-
Stores user key metadata locally.
bindForJourney options
| Option | Required | Description |
|---|---|---|
|
No |
Zero-based index of the |
|
No |
A human-readable label for the device shown in the server’s registered devices list. Defaults to the device model name when omitted. |
|
No |
Override the signing algorithm for Android devices. It uses the server-configured default when omitted. This property is ignored by the iOS SDK, which uses ES256 exclusively. |
|
No |
A |
|
No |
Override biometric authentication parameters. |
|
No |
Override JWT generation parameters. |
Collecting a device name from the user
In a profile management journey you may want to let the user choose a name for their device. Detect the DeviceBindingCallback field in useJourneyForm and render a text input for the device name:
import { useJourneyForm } from '@ping-identity/rn-journey';
const form = useJourneyForm(node);
const bindingField = form.getFieldByType('DeviceBindingCallback');
// The field has executionMode: 'integration_required'
// Render a text input and pass the entered value to bindForJourney
async function handleBind(enteredDeviceName: string) {
await bindingClient.bindForJourney(journeyClient, {
index: bindingField?.ref.typeIndex ?? 0,
deviceName: enteredDeviceName.trim() || undefined,
});
await actions.next();
}
|
|
Verifying bound keys on a device
When a journey node contains a Device Signing Verifier step, the journey callback type is DeviceSigningVerifierCallback. Call bindingClient.signForJourney() to sign the server-provided challenge using the locally stored private key:
async function handleDeviceSigning() {
await bindingClient.signForJourney(journeyClient);
// The callback has been fulfilled — advance the journey
await actions.next();
}
The signing process performs the following steps:
-
Validates any custom claims provided
-
Looks up the appropriate user key for the current user
-
Authenticates the user (biometric, PIN, or none)
-
Signs the server challenge with the private key
-
Creates a verification JWT
signForJourney options
| Option | Required | Description |
|---|---|---|
|
No |
Zero-based index of the |
|
No |
Custom key-value pairs added to the verification JWT. See Adding custom claims when signing. |
|
No |
Override the signing algorithm. |
|
No |
Pre-supply an application PIN string. |
|
No |
Override biometric authentication parameters. |
|
No |
Override JWT generation parameters. |
Adding custom claims when signing
When signing the server challenge you can add custom data to the resulting JWT. The server can use these claims for context or auditing:
await bindingClient.signForJourney(journeyClient, {
claims: {
amount: '100.00',
recipient: 'babs@example.com',
currency: 'USD',
},
});
|
You cannot use reserved JWT claim names ( |
Implementing the UI for PIN and key selection
The Device Binding module does not include built-in PIN or key-selection screens, so you must supply your own UI through two functions passed in the ui object of createBindingClient. The pinCollector function is required whenever the server configures APPLICATION_PIN authentication. The userKeySelector function handles the parallel RNPingBinding_UserKeyRequired event that fires when multiple users have bound keys on the same device.
Providing a custom PIN collector
When the server configures APPLICATION_PIN authentication, the native SDK emits a RNPingBinding_PinRequired event requesting a PIN from the user. You must supply a pinCollector function in createBindingClient configuration because the module does not have a built-in PIN UI.
The pinCollector receives a BindingPrompt that is populated with configuration values from the server, and must return a Promise<string> that resolves to the PIN the user entered, or rejects to cancel the operation:
import { createBindingClient } from '@ping-identity/rn-binding';
import type { BindingPrompt } from '@ping-identity/rn-binding';
// Simple imperative promise resolver — replace with your UI implementation
let _resolvePinPromise: ((pin: string) => void) | null = null;
let _rejectPinPromise: (() => void) | null = null;
const bindingClient = createBindingClient({
ui: {
pinCollector: async (prompt: BindingPrompt): Promise<string> => {
// Show your PIN modal here; resolve when the user submits
return new Promise((resolve, reject) => {
_resolvePinPromise = resolve;
_rejectPinPromise = reject;
showPinModal(prompt); // your UI function
});
},
},
});
BindingPrompt properties
| Property | Type | Description |
|---|---|---|
|
|
Modal heading. Typically |
|
|
Secondary heading line. |
|
|
Instructional text shown below the heading. |
Example PIN collector component
Calling pinCollector bypasses the native default PIN dialog and calls your implementation instead. The prompt argument supplies title, subtitle, and description strings
sourced from the server-side journey callback.
import { createBindingClient } from '@ping-identity/rn-binding';
import type { BindingPrompt } from '@ping-identity/rn-binding';
const pinCollector = (prompt: BindingPrompt): Promise<string> => {
// prompt.title - dialog title from the server callback
// prompt.subtitle - dialog subtitle from the server callback
// prompt.description - dialog description from the server callback
return new Promise((resolve, reject) => {
// present your own PIN input UI here
// call resolve(pin) with the entered PIN string
// call reject() if the user cancels
});
};
export const bindingClient = createBindingClient({
ui: {
pinCollector,
},
});
Providing a custom key selector
When multiple users have bound keys on the same device, the native SDK emits a RNPingBinding_UserKeyRequired event to ask the user which key to use. You can supply a userKeySelector function to replace React Native’s default behavior, which delegates key selection to the native platform UI:
import { createBindingClient } from '@ping-identity/rn-binding';
import type { UserKeyOption } from '@ping-identity/rn-binding';
const bindingClient = createBindingClient({
ui: {
userKeySelector: async (keys: UserKeyOption[]): Promise<UserKeyOption> => {
// Show a list of keys to the user and resolve with the selected one.
// Reject or throw to cancel the operation.
return showKeySelectionSheet(keys);
},
},
});
UserKeyOption properties
| Property | Type | Description |
|---|---|---|
|
|
Unique key identifier (kid) used for deletion. |
|
|
The user identifier associated with this key. |
|
|
Human-readable username shown in selection UI. |
|
|
Authentication type configured for this key (for example, |
Managing locally stored keys
The module exposes three key-management functions that operate on locally stored binding key metadata. These are useful for building device management screens.
Listing all keys
import { getAllKeys } from '@ping-identity/rn-binding';
const keys = await getAllKeys();
// Returns UserKeyOption[]
keys.forEach((key) => {
console.log(`${key.username} (${key.authenticationType}) — id: ${key.id}`);
});
Deleting a single key
import { deleteKey } from '@ping-identity/rn-binding';
await deleteKey(key); // key is a UserKeyOption returned by getAllKeys()
Deleting all keys
import { deleteAllKeys } from '@ping-identity/rn-binding';
await deleteAllKeys();
Key management functions
import {
getAllKeys,
deleteKey,
deleteAllKeys,
} from '@ping-identity/rn-binding';
import type { UserKeyOption } from '@ping-identity/rn-binding';
/**
* Retrieves all device binding keys stored locally on the device.
* Returns an array of UserKeyOption, each with id, userId, username,
* and authenticationType fields.
*/
export const fetchAllKeys = async (): Promise<UserKeyOption[]> => {
return await getAllKeys();
};
/**
* Deletes a single device binding key by its userId and id.
* Pass a UserKeyOption obtained from getAllKeys().
*/
export const removeKey = async (key: UserKeyOption): Promise<void> => {
await deleteKey(key);
};
/**
* Deletes all locally stored device binding keys.
* Use with caution — this cannot be undone and will require
* the user to re-bind their device.
*/
export const removeAllKeys = async (): Promise<void> => {
await deleteAllKeys();
};
Handling errors
bindForJourney and signForJourney both reject with a BindingError when the native operation fails.
import { BindingError } from '@ping-identity/rn-binding';
try {
await bindingClient.bindForJourney(journeyClient, { deviceName });
} catch (err) {
if (err instanceof BindingError) {
console.error(`[${err.code}] ${err.message}`);
handleBindingError(err.code);
} else {
throw err;
}
}
Error codes
| Error code | Cause | Remediation |
|---|---|---|
|
The device lacks required capabilities, or the user has not enrolled biometrics. |
Retry with an alternative authentication type, or prompt the user to enroll biometrics in device settings. |
|
No keys are available for signing. The device has not been bound, or the user removed the authentication method protecting the private key. |
Redirect the user to run a binding journey to register a new key. |
|
The operation exceeded the configured timeout. |
Allow retry, optionally with a longer timeout. |
|
Reserved claim names ( |
Remove or rename the conflicting claim keys. |
|
The user cancelled the operation, for example by tapping Cancel on the biometric prompt. |
Handle gracefully without showing an error — the user chose not to continue. |
|
The user provided invalid credentials, for example an incorrect PIN. |
Allow retry and prompt for the correct credentials. |