Orchestration SDKs

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 DeviceBindingCallback when 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 DeviceSigningVerifierCallback when 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

Supported authentication types to access bound keys
  • Biometric Only

  • Biometric with Fallback

  • Application PIN

  • No Authentication

Type name

BIOMETRIC_ONLY

Description

Requires strict biometric authentication with no fallback options

Security level

High

User experience

Streamlined for devices with reliable biometric sensors

Behavior
  • Only accepts biometric authentication, such as a fingerprint, face recognition, or an iris scan

  • Fails immediately if biometric authentication is unavailable or unsuccessful

  • No option to fall back to device PIN, pattern, or password

  • Ideal for high-security applications where biometric verification is mandatory

Use cases

Financial applications, enterprise security, medical applications

Device requirements

Must have functional biometric sensors and enrolled biometric data

Type name

BIOMETRIC_ALLOW_FALLBACK

Description

Prefers biometric authentication but allows fallback to device credentials

Security level

Medium to High

User experience

Flexible with multiple authentication options

Behavior
  • The primary method is a biometric authentication, such as a fingerprint, face recognition, or an iris scan

  • If biometric authentication fails or is unavailable, users can use device credentials

    • Device credentials include a PIN, a pattern, or a password set at the system level

  • Provides better accessibility and usability

Use cases

Consumer applications, general-purpose authentication, accessibility-focused apps

Device requirements
  • Biometric sensors preferred, but not required

  • Must configure the device lock screen

Type name

APPLICATION_PIN

Description

Requires a custom PIN that the application manages entirely

Security level

Medium

User experience

Consistent across all devices regardless of hardware capabilities

Behavior
  • Uses an application-specific PIN separate from device credentials

    • The application collects the PIN through a custom UI

    • The application securely stores PIN data using encrypted storage mechanisms

    • Independent of device biometric capabilities or system-level authentication

Use cases
  • Devices without biometric capabilities

  • Applications requiring custom authentication flows

  • Scenarios where users prefer PIN over biometric authentication

    • Cross-platform consistency requirements

Device requirements

None - works on all devices

Type name

NONE

Description

No user authentication required to access cryptographic keys

Security Level

Low

User Experience

Seamless with no authentication prompts

Behavior
  • Users can access keys immediately without any verification

  • No authentication prompts or delays

  • Cryptographic operations proceed without user interaction

  • Relies solely on device possession for security

Use cases
  • Applications with alternative security measures

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:

Creating the device binding client
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

logger

No

A LoggerInstance created by logger({ level }) from @ping-identity/rn-logger.

Provides diagnostic output for binding operations.

ui.pinCollector

No

A function (prompt: BindingPrompt) => Promise<string> that collects a PIN from the user.

Replaces the default native PIN dialog. See Providing a custom PIN collector.

ui.userKeySelector

No

A function (keys: UserKeyOption[]) => Promise<UserKeyOption> that presents available keys for the user to select.

Replaces the default native key selector. See Providing a custom key selector.

userKeyStorage

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:

Binding cryptographic keys to a device during a journey
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:

  1. Validates device support for the requested authentication type.

  2. Removes any existing keys for the user on this device.

  3. Generates a new cryptographic key pair.

  4. Authenticates the user (biometric, PIN, or none, depending on server configuration).

  5. Creates a signed proof-of-possession JWT.

  6. Stores user key metadata locally.

bindForJourney options

Option Required Description

index

No

Zero-based index of the DeviceBindingCallback within the node, when a node contains multiple binding callbacks. Defaults to 0.

deviceName

No

A human-readable label for the device shown in the server’s registered devices list. Defaults to the device model name when omitted.

signingAlgorithm

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.

appPin

No

A BindingAppPinConfig object that configures the pin dialog behavior.

biometric

No

Override biometric authentication parameters.

jwt

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:

Collecting a device name from the user during binding
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();
}

DeviceBindingCallback has executionMode: 'integration_required' but also exposes a device-name input field that is visible to the user. It is not auto-forwarded by default — the user must confirm the device name before binding proceeds. Contrast this with DeviceSigningVerifierCallback, which is fully auto-forwardable.

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:

Signing a server challenge with a bound device 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:

  1. Validates any custom claims provided

  2. Looks up the appropriate user key for the current user

  3. Authenticates the user (biometric, PIN, or none)

  4. Signs the server challenge with the private key

  5. Creates a verification JWT

signForJourney options

Option Required Description

index

No

Zero-based index of the DeviceSigningVerifierCallback within the node. Defaults to 0.

claims

No

Custom key-value pairs added to the verification JWT. See Adding custom claims when signing.

signingAlgorithm

No

Override the signing algorithm.

appPin

No

Pre-supply an application PIN string.

biometric

No

Override biometric authentication parameters.

jwt

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:

Adding custom claims to the signing JWT
await bindingClient.signForJourney(journeyClient, {
  claims: {
    amount: '100.00',
    recipient: 'babs@example.com',
    currency: 'USD',
  },
});

You cannot use reserved JWT claim names (sub, exp, iat, iss, nbf, and challenge) as custom claim keys. Doing so causes the operation to fail with an invalidCustomClaims error.

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:

Providing a custom PIN collector to the binding client
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

title

string

Modal heading. Typically "Enter PIN" or similar server-configured text.

subtitle

string

Secondary heading line.

description

string

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.

Implementing a PIN collector component
  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:

Providing a custom key selector for multi-user devices
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

id

string

Unique key identifier (kid) used for deletion.

userId

string

The user identifier associated with this key.

username

string

Human-readable username shown in selection UI.

authenticationType

string

Authentication type configured for this key (for example, BIOMETRIC_ONLY, APPLICATION_PIN).

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

Listing all locally stored binding 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

Deleting a single binding key by UserKeyOption
import { deleteKey } from '@ping-identity/rn-binding';

await deleteKey(key);   // key is a UserKeyOption returned by getAllKeys()

Deleting all keys

Deleting all locally stored binding keys
import { deleteAllKeys } from '@ping-identity/rn-binding';

await deleteAllKeys();

Key management functions

Using key management functions together
  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.

Catching and handling BindingError from journey operations
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

UNSUPPORTED

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.

CLIENT_NOT_REGISTERED

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.

TIMEOUT

The operation exceeded the configured timeout.

Allow retry, optionally with a longer timeout.

INVALID_CUSTOM_CLAIMS

Reserved claim names (sub, exp, iat, iss, and so on) were used in claims.

Remove or rename the conflicting claim keys.

ABORT

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.

UNAUTHORIZE

The user provided invalid credentials, for example an incorrect PIN.

Allow retry and prompt for the correct credentials.