This package provides native-backed device binding and signing-verifier capabilities for React Native.
useJourneyForm integrationNote: This module requires that the
@ping-identity/rn-coremodule is already set up and installed.
# Install & setup the core module
yarn add @ping-identity/rn-core
# Install the rn-binding module
yarn add @ping-identity/rn-binding
# If you are developing your app using iOS, run this command
cd ios && pod install
Optional integration packages:
yarn add @ping-identity/rn-logger
yarn add @ping-identity/rn-storage
Run Journey binding callbacks explicitly before journey.next(...).
import { createBindingClient } from '@ping-identity/rn-binding';
const binding = createBindingClient();
if (node.type === 'ContinueNode') {
for (const callback of node.callbacks ?? []) {
if (callback.type === 'DeviceBindingCallback') {
await binding.bindForJourney(journey, { index: 0 });
}
if (callback.type === 'DeviceSigningVerifierCallback') {
await binding.signForJourney(journey, { index: 0 });
}
}
await journey.next({});
}
import { createBindingClient } from '@ping-identity/rn-binding';
import { logger } from '@ping-identity/rn-logger';
const binding = createBindingClient({
logger: logger({ level: 'debug' }),
});
useJourneyForm integrationWhen using useJourneyForm, pass handledCallbackTypes so binding fields are excluded from
blocking submit issues. Run each integration, then submit when form.canSubmit is true.
import { useJourney, useJourneyForm } from '@ping-identity/rn-journey';
import { createBindingClient } from '@ping-identity/rn-binding';
import { nativeExtensionCallbackType } from '@ping-identity/rn-types';
const [node, actions] = useJourney(client);
const form = useJourneyForm(node, {
handledCallbackTypes: new Set([
nativeExtensionCallbackType.DeviceBindingCallback,
nativeExtensionCallbackType.DeviceSigningVerifierCallback,
]),
});
const binding = createBindingClient();
for (const field of form.fields) {
if (field.ref.type === nativeExtensionCallbackType.DeviceBindingCallback) {
await binding.bindForJourney(journey, { index: field.ref.typeIndex });
}
if (
field.ref.type === nativeExtensionCallbackType.DeviceSigningVerifierCallback
) {
await binding.signForJourney(journey, { index: field.ref.typeIndex });
}
}
if (form.canSubmit) {
await actions.next(form.input);
}
Use pinCollector and userKeySelector only when you want app-owned UI.
When omitted, the native SDK default UI is used on each platform.
import {
createBindingClient,
type BindingPrompt,
type UserKeyOption,
} from '@ping-identity/rn-binding';
const binding = createBindingClient({
ui: {
pinCollector: async (prompt: BindingPrompt) => {
// Show app PIN modal and resolve with the entered PIN.
return showPinModal(prompt);
},
userKeySelector: async (keys: UserKeyOption[]) => {
// Show app key-selection UI and resolve with the chosen key.
return showKeySelector(keys);
},
},
});
By default, key metadata is stored in a shared location. Pass a custom storage handle
(created by configureBindingUserKeyStorage from @ping-identity/rn-storage) to isolate
key storage per app or configuration.
import { createBindingClient } from '@ping-identity/rn-binding';
import { configureBindingUserKeyStorage } from '@ping-identity/rn-storage';
const binding = createBindingClient({
userKeyStorage: configureBindingUserKeyStorage({
android: {
keyAlias: 'binding_user_keys',
fileName: 'binding_user_keys',
strongBoxPreferred: true,
cacheStrategy: 'no_cache',
},
ios: {
account: 'com.example.binding.keys',
encryptor: true,
},
}),
});
After a successful bind, a key record is stored locally on the device. Use these module-level utilities to inspect and clean up stored keys — for example when a user logs out, de-registers a device, or you need to recover from a corrupted binding state.
import {
getAllKeys,
deleteKey,
deleteAllKeys,
type UserKeyOption,
} from '@ping-identity/rn-binding';
// Retrieve all locally stored binding keys
const keys: UserKeyOption[] = await getAllKeys();
// [{ id: 'abc123', userId: 'user-001', username: 'alice', authenticationType: 'BIOMETRIC_ONLY' }]
// Delete a single key (pass the UserKeyOption returned by getAllKeys)
await deleteKey(keys[0]);
// Delete all locally stored keys
await deleteAllKeys();
These functions are stateless utilities — they do not require a BindingClient instance.
On both platforms, deleting a key removes:
Deleting a key locally does not remove the device registration record from the server.
If you need to deregister the device server-side, use the device-client package first, then
call deleteKey to clean up the local key.
deleteKey rejects with BINDING_KEY_DELETE_ERROR if the specified key is not found.
try {
const keys = await getAllKeys();
if (keys.length > 0) {
await deleteKey(keys[0]);
}
} catch (err) {
if (err instanceof BindingError) {
// err.code === 'BINDING_KEY_DELETE_ERROR'
}
}
appPin, biometric, jwt, and signingAlgorithm are per-call options passed directly to
bindForJourney or signForJourney. Options marked // iOS only or // Android only are
silently ignored on the other platform.
import {
createBindingClient,
type BindingPrompt,
type UserKeyOption,
} from '@ping-identity/rn-binding';
import { configureBindingUserKeyStorage } from '@ping-identity/rn-storage';
import { logger } from '@ping-identity/rn-logger';
const binding = createBindingClient({
logger: logger({ level: 'debug' }),
ui: {
pinCollector: async (prompt: BindingPrompt) => showPinModal(prompt),
userKeySelector: async (keys: UserKeyOption[]) => showKeySelector(keys),
},
userKeyStorage: configureBindingUserKeyStorage({
android: { keyAlias: 'binding_user_keys', fileName: 'binding_user_keys' },
ios: { account: 'com.example.binding.keys', encryptor: true },
}),
});
// Bind — all authenticator options are per-call
await binding.bindForJourney(journey, {
deviceName: 'My Phone',
signingAlgorithm: 'RS256', // Android only
appPin: {
maxAttempts: 5,
keystoreType: 'PKCS12', // Android only
keyTag: 'com.example.apppin.key', // iOS only
prompt: {
title: 'Enter PIN',
subtitle: 'Verify your identity',
description: 'Enter your application PIN',
},
},
biometric: {
android: {
strongBoxPreferred: true, // Android only
prompt: {
title: 'Verify identity',
subtitle: 'Authentication required',
description: 'Use your biometric to continue',
negativeButtonText: 'Cancel', // shown for BIOMETRIC_ONLY auth type
},
},
ios: {
keyTag: 'com.example.biometric.key',
},
},
jwt: {
issueTimeEpochSeconds: Math.floor(Date.now() / 1000),
notBeforeTimeEpochSeconds: Math.floor(Date.now() / 1000),
expirationTimeEpochSeconds: Math.floor(Date.now() / 1000) + 300,
},
});
// Sign — same options plus claims
await binding.signForJourney(journey, {
claims: {
transaction_id: 'txn_abc123',
amount: '100.00',
},
signingAlgorithm: 'RS256', // Android only
appPin: {
maxAttempts: 5,
},
biometric: {
android: {
strongBoxPreferred: true,
},
},
jwt: {
issueTimeEpochSeconds: Math.floor(Date.now() / 1000),
expirationTimeEpochSeconds: Math.floor(Date.now() / 1000) + 300,
},
});
import {
createBindingClient,
getAllKeys,
deleteKey,
deleteAllKeys,
} from '@ping-identity/rn-binding';
import type {
BindingClient,
BindingConfig,
BindingJourneyBindOptions,
BindingJourneySignOptions,
BindingJourneyResult,
JourneyInstance,
UserKeyOption,
} from '@ping-identity/rn-binding';
// Journey binding client
function createBindingClient(config?: BindingConfig): BindingClient;
interface BindingClient {
bindForJourney(
journey: JourneyInstance,
options?: BindingJourneyBindOptions,
): Promise<BindingJourneyResult>;
signForJourney(
journey: JourneyInstance,
options?: BindingJourneySignOptions,
): Promise<BindingJourneyResult>;
}
// Key management utilities (no client instance required)
function getAllKeys(): Promise<UserKeyOption[]>;
function deleteKey(key: UserKeyOption): Promise<void>;
function deleteAllKeys(): Promise<void>;
type UserKeyOption = {
id: string; // Unique key identifier (kid)
userId: string; // User identifier associated with this key
username: string; // Human-readable username associated with this key
authenticationType: string; // e.g. "BIOMETRIC_ONLY", "APPLICATION_PIN", "NONE"
};
createBindingClient)The client holds only stateful, shared concerns. Per-operation options are passed directly to
bindForJourney / signForJourney.
| Option | Type | Description |
|---|---|---|
logger |
LoggerInstance |
JS logger from @ping-identity/rn-logger. |
ui.pinCollector |
(prompt) => Promise<string> |
App-owned PIN collection UI callback. |
ui.userKeySelector |
(keys) => Promise<UserKeyOption> |
App-owned user-key selection callback. |
userKeyStorage |
UserKeyStorage |
Handle returned by configureBindingUserKeyStorage(...). |
bindForJourney)| Option | Type | Default | Description |
|---|---|---|---|
index |
number |
0 |
Callback index when multiple bind callbacks exist. |
deviceName |
string |
Device model name | Human-readable device name sent to the server. |
signingAlgorithm |
string |
"RS512" |
JWS algorithm for the signed JWT proof. Android only; iOS always uses ES256. |
appPin |
BindingAppPinConfig |
— | App PIN authenticator options. |
biometric |
BindingBiometricBindConfig |
— | Biometric authenticator options. |
jwt |
BindingJwtConfig |
— | JWT proof timing options. |
signForJourney)| Option | Type | Default | Description |
|---|---|---|---|
index |
number |
0 |
Callback index when multiple sign callbacks exist. |
claims |
Record<string, unknown> |
{} |
Custom claims added to the signed JWT payload. |
signingAlgorithm |
string |
"RS512" |
JWS algorithm for the signed JWT proof. Android only; iOS always uses ES256. |
appPin |
BindingAppPinConfig |
— | App PIN authenticator options. |
biometric |
BindingBiometricSignConfig |
— | Biometric authenticator options. |
jwt |
BindingJwtConfig |
— | JWT proof timing options. |
Reserved claim names (sub, exp, iat, nbf, iss, challenge) cannot be used in claims.
BindingAppPinConfig| Option | Type | Platform | Description |
|---|---|---|---|
maxAttempts |
number |
Both | Maximum PIN retry count. |
prompt.title |
string |
Both | PIN prompt title override. |
prompt.subtitle |
string |
Both | PIN prompt subtitle override. |
prompt.description |
string |
Both | PIN prompt description override. |
keyTag |
string |
iOS only, bind only | Keychain key tag for the app PIN authenticator key. Stored at bind time; used automatically on sign. |
keystoreType |
string |
Android only | KeyStore type for app PIN key storage. Defaults to "PKCS12". |
BindingBiometricBindConfig / BindingBiometricSignConfig| Option | Type | Platform | Description |
|---|---|---|---|
android.prompt.title |
string |
Android | Biometric prompt title. |
android.prompt.subtitle |
string |
Android | Biometric prompt subtitle. |
android.prompt.description |
string |
Android | Biometric prompt description. |
android.prompt.negativeButtonText |
string |
Android | Cancel button label (shown for BIOMETRIC_ONLY). |
android.strongBoxPreferred |
boolean |
Android | Prefer StrongBox-backed key storage. Defaults to false. |
ios.keyTag |
string |
iOS, bind only | Keychain key tag for the biometric authenticator key. Stored at bind time; used automatically on sign. |
BindingJwtConfig| Option | Type | Platform | Description |
|---|---|---|---|
issueTimeEpochSeconds |
number |
Both | Fixed iat claim value. |
notBeforeTimeEpochSeconds |
number |
Both | Fixed nbf claim value. |
expirationTimeEpochSeconds |
number |
Both | Fixed exp claim value. |
Rejected promises throw a BindingError instance, which extends PingError extends Error. Use instanceof BindingError to narrow in catch blocks.
Stable error codes:
| Code | Description |
|---|---|
BINDING_ERROR |
Unexpected error. |
BINDING_BIND_ERROR |
Bind operation failed. |
BINDING_SIGN_ERROR |
Sign operation failed. |
BINDING_CANCELLED |
User cancelled the authentication prompt, or the operation timed out. |
BINDING_UNSUPPORTED_DEVICE |
Device lacks the hardware or OS capabilities required for binding. |
BINDING_NOT_REGISTERED |
No local key found for this user on this device — device must be bound first. |
BINDING_KEY_INVALIDATED |
The signing key on this device is no longer usable — typically caused by a biometric enrollment change. The user must re-bind; their account and other devices are unaffected. |
BINDING_AUTH_FAILED |
Authentication failed — wrong fingerprint, wrong PIN, or biometric lockout. The key is intact and the operation can be retried. |
BINDING_UI_UNAVAILABLE |
No foreground Activity (Android) or UIWindowScene (iOS) available. |
BINDING_CALLBACK_NOT_FOUND |
No matching Journey callback found for the given journey id and index. |
BINDING_INVALID_CONFIG |
Reserved claim names were used in claims, or another configuration error. |
BINDING_KEY_DELETE_ERROR |
deleteKey could not find the specified key in local storage. |
BINDING_KEY_INVALIDATED and BINDING_NOT_REGISTERED both mean the key on this device is gone — the user must re-bind. The user's account and any keys bound on other devices are unaffected.
import { BindingError } from '@ping-identity/rn-binding';
try {
await binding.signForJourney(journey);
} catch (err) {
if (
err instanceof BindingError &&
(err.code === 'BINDING_KEY_INVALIDATED' ||
err.code === 'BINDING_NOT_REGISTERED')
) {
// Key is gone on this device — clean up and re-bind.
const keys = await getAllKeys();
const stale = keys.find((k) => k.userId === currentUserId);
if (stale) await deleteKey(stale);
// redirect user to bind flow
}
}
Activity for binding and signing calls.UIWindowScene/ASPresentationAnchor for binding and signing calls.The package ships with the native SDK's default UI for each authenticator type:
| Authenticator (server-configured) | UI shown |
|---|---|
BIOMETRIC_ONLY |
System biometric prompt (Android BiometricPrompt / iOS LAContext). |
BIOMETRIC_ALLOW_FALLBACK |
Biometric prompt with device-credential fallback. |
APPLICATION_PIN |
SDK-built PIN entry dialog. |
NONE |
No UI; keys are generated without user prompt. |
When multiple registered keys exist for a signForJourney operation, the SDK presents a built-in picker dialog so the user can choose which key to sign with.
The authenticator type is configured on the AM Journey node, not from JavaScript.
biometric authenticator is selected.This project is licensed under the MIT License - see the LICENSE file for details