This package provides a native-backed FIDO bridge 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-fido module
yarn add @ping-identity/rn-fido
# If you are developing your app using iOS, run this command
cd ios && pod install
Optional integration packages:
yarn add @ping-identity/rn-logger
https://<rp-domain>/.well-known/assetlinks.json.assetlinks.json contains your Android package name and signing cert fingerprint(s).androidx.credentials:credentials-play-services-auth.com.google.android.gms:play-services-fido.webcredentials:<rp-domain>).https://<rp-domain>/.well-known/apple-app-site-association.Use createFidoClient(config?) and call operations on the returned client.
import { createFidoClient } from '@ping-identity/rn-fido';
import { logger } from '@ping-identity/rn-logger';
const log = logger({ level: 'debug' });
const fido = createFidoClient({
logger: log,
android: {
useFido2Client: true,
},
});
const registrationResult = await fido.register({
challenge: 'base64url-challenge',
rp: { id: 'example.com', name: 'Example Inc.' },
user: {
id: 'base64url-user-id',
name: 'user@example.com',
displayName: 'Example User',
},
pubKeyCredParams: [{ type: 'public-key', alg: -7 }],
});
const authenticationResult = await fido.authenticate({
challenge: 'base64url-challenge',
rpId: 'example.com',
allowCredentials: [],
});
If you install the logger package, pass a JS logger instance created via
@ping-identity/rn-logger.
If the logger package is not installed/configured, do not pass logger values in FIDO config.
JavaScript-side FIDO logs use this logger on both platforms.
Native logger forwarding applies to standalone operations on both platforms.
Journey and DaVinci collector ceremonies retain their workflow-configured native logger.
import { createFidoClient } from '@ping-identity/rn-fido';
import { logger } from '@ping-identity/rn-logger';
const jsLogger = logger({ level: 'debug' });
const fido = createFidoClient({
logger: jsLogger,
});
import { createFidoClient } from '@ping-identity/rn-fido';
const fidoA = createFidoClient({
android: { useFido2Client: true },
});
const fidoB = createFidoClient({
android: { useFido2Client: false },
});
await fidoA.register({ challenge: '...' });
await fidoB.authenticate({ challenge: '...' });
Run Journey FIDO callbacks explicitly before journey.next(...).
import { createFidoClient } from '@ping-identity/rn-fido';
const fido = createFidoClient();
if (node.type === 'ContinueNode') {
for (const callback of node.callbacks ?? []) {
if (callback.type === 'FidoRegistrationCallback') {
await fido.registerForJourney(journey, {
index: 0,
deviceName: 'My Device',
});
}
if (callback.type === 'FidoAuthenticationCallback') {
await fido.authenticateForJourney(journey, { index: 0 });
}
}
await journey.next({});
}
FIDO2 DaVinci collectors use one collector type and an action discriminator:
action: 'REGISTER' exposes creation options, while action: 'AUTHENTICATE'
exposes request options. Create a FIDO client before starting or normalizing
the DaVinci flow — createFidoClient registers fidoCollectorType
('FIDO2') with the integration registry and eagerly registers the native
collector serializer, so nodes mapped before the first ceremony still carry
action and the WebAuthn options payload.
Run the ceremony, then advance the DaVinci flow with an empty collector input:
import { useDaVinciForm } from '@ping-identity/rn-davinci';
import {
createFidoClient,
fidoCollectorType,
type FidoCollector,
} from '@ping-identity/rn-fido';
const fido = createFidoClient();
const form = useDaVinciForm(node, {
handledCollectorTypes: new Set([fidoCollectorType]),
});
const collector = node.collectors[0] as FidoCollector;
if (collector.action === 'REGISTER') {
await fido.registerForDaVinci(daVinci, { index: 0 });
} else {
await fido.authenticateForDaVinci(daVinci, { index: 0 });
}
// The native collector retains the attestation or assertion for submission.
await daVinci.next({ collectors: [] });
When using useDaVinciForm, include fidoCollectorType in
handledCollectorTypes after creating the FIDO client. Do not pass the FIDO
collector key to next(). The initial DaVinci integration uses native ceremony
defaults and does not expose React Native customization options.
The optional index selects among multiple FIDO2 collectors with the same
action. The methods return the native attestation or assertion payload for
informational use; the native collector submits it when the flow advances.
When a DaVinci ceremony fails, the native collector classifies the failure and
the rejection carries FidoError.clientError, a WebAuthn DOMException name
(for example 'NotAllowedError' after a user cancellation). When
clientError is defined, report the failure to the server by advancing the
flow with daVinci.next({ collectors: [] }) so the DaVinci flow can branch on
it; without propagation the failure stays client-side and the server never
observes it. Resolution failures such as FIDO_COLLECTOR_NOT_FOUND carry no
clientError and must not be propagated; keep the node rendered for retry.
Always advance with an empty collector input. Never submit the node's
collectors after a failed ceremony: on a node that also contains a
SUBMIT_BUTTON, the submit action shadows the FIDO error and the server
receives a normal submit instead of the failure (the native event builder
picks a submit or flow collector's action first, before any FIDO error
action).
import { FidoError } from '@ping-identity/rn-fido';
try {
if (collector.action === 'REGISTER') {
await fido.registerForDaVinci(daVinci, { index: 0 });
} else {
await fido.authenticateForDaVinci(daVinci, { index: 0 });
}
await daVinci.next({ collectors: [] });
} catch (error) {
const err = FidoError.from(error);
if (err.clientError !== undefined) {
// The collector classified the failure; submit it so the flow can branch.
try {
await daVinci.next({ collectors: [] });
} catch (propagationError) {
// Advancing the flow also failed; surface it without losing the
// original ceremony failure.
}
}
// Handle the failure in place, for example by showing err.message. A
// failure without clientError is not propagatable; keep the node rendered
// for retry. Do not propagate twice: once submitted, the failure state is
// consumed.
}
FidoError.clientError values and their typical outcomes:
clientError |
Typical outcome |
|---|---|
NotAllowedError |
User cancellation or dismissal of the passkey UI. |
TimeoutError |
The ceremony timed out. |
NotSupportedError |
Passkeys are unsupported on the device or blocked by policy. |
InvalidStateError |
The credential is invalid or already registered (excluded credential). |
UnknownError |
Any other failure, including an unrecognized native error. |
NotAllowedError, TimeoutError, NotSupportedError, InvalidStateError,
and UnknownError are the only values iOS reports. Android reports an open
set: credential exceptions map to their WebAuthn DOMException names, so other
valid names (for example SecurityError) can appear. Unknown names pass
through to the server unmodified.
useJourneyForm integrationWhen using useJourneyForm, pass handledCallbackTypes so FIDO 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 { createFidoClient } from '@ping-identity/rn-fido';
import { nativeExtensionCallbackType } from '@ping-identity/rn-types';
const [node, actions] = useJourney(client);
const form = useJourneyForm(node, {
handledCallbackTypes: new Set([
nativeExtensionCallbackType.FidoRegistrationCallback,
nativeExtensionCallbackType.FidoAuthenticationCallback,
]),
});
const fido = createFidoClient();
for (const field of form.fields) {
if (field.ref.type === nativeExtensionCallbackType.FidoRegistrationCallback) {
await fido.registerForJourney(journey, { index: field.ref.typeIndex });
}
if (
field.ref.type === nativeExtensionCallbackType.FidoAuthenticationCallback
) {
await fido.authenticateForJourney(journey, { index: field.ref.typeIndex });
}
}
if (form.canSubmit) {
await actions.next(form.input);
}
import { createFidoClient } from '@ping-identity/rn-fido';
import type {
FidoClient,
FidoConfig,
FidoRegistrationOptions,
FidoRegistrationResult,
FidoAuthenticationOptions,
FidoAuthenticationResult,
FidoJourneyRegistrationOptions,
FidoJourneyAuthenticationOptions,
FidoJourneyResult,
FidoDaVinciRegistrationOptions,
FidoDaVinciAuthenticationOptions,
FidoDaVinciResult,
DaVinciInstance,
JourneyInstance,
} from '@ping-identity/rn-fido';
function createFidoClient(config?: FidoConfig): FidoClient;
interface FidoClient {
register(options: FidoRegistrationOptions): Promise<FidoRegistrationResult>;
authenticate(
options: FidoAuthenticationOptions,
): Promise<FidoAuthenticationResult>;
registerForJourney(
journey: JourneyInstance,
options?: FidoJourneyRegistrationOptions,
): Promise<FidoJourneyResult>;
authenticateForJourney(
journey: JourneyInstance,
options?: FidoJourneyAuthenticationOptions,
): Promise<FidoJourneyResult>;
registerForDaVinci(
daVinci: DaVinciInstance,
options?: FidoDaVinciRegistrationOptions,
): Promise<FidoDaVinciResult>;
authenticateForDaVinci(
daVinci: DaVinciInstance,
options?: FidoDaVinciAuthenticationOptions,
): Promise<FidoDaVinciResult>;
}
Rejected promises throw a FidoError instance, which extends PingError extends Error. Use instanceof FidoError to narrow in catch blocks.
Stable error codes:
FIDO_ERRORFIDO_REGISTER_ERRORFIDO_AUTHENTICATE_ERRORFIDO_AUTHENTICATE_CANCELLEDFIDO_ACTIVITY_UNAVAILABLE (Android)FIDO_WINDOW_UNAVAILABLE (iOS)FIDO_CALLBACK_NOT_FOUNDFIDO_COLLECTOR_NOT_FOUNDDaVinci ceremony failures additionally carry FidoError.clientError, the
WebAuthn DOMException name reported by the native collector, typed as
FidoClientErrorName. The two fields classify the same failure at different
layers and can intentionally disagree: code is the bridge-local
classification (for example FIDO_AUTHENTICATE_CANCELLED), while
clientError is the server-parity value the DaVinci flow branches on (for
example, on Android a missing credential reports clientError: 'UnknownError'
next to code: 'FIDO_AUTHENTICATE_CANCELLED').
clientError is defined only on DaVinci ceremony failures that the collector
classified. Standalone and Journey rejections never carry it, and neither do
resolution failures such as FIDO_COLLECTOR_NOT_FOUND. See
DaVinci ceremony failure propagation
for the propagation contract and the full value table.
android.useFido2Client is an Android-only override for the standalone register / authenticate operations.
undefined (default): native SDK auto-detection/default behavior.true: force Google Play Services FIDO2 APIs.false: force Android Credential Manager APIs.registerForJourney, authenticateForJourney, registerForDaVinci, authenticateForDaVinci) currently ignore the client-level config on both platforms: ceremonies keep the workflow-configured native logger and native client defaults, so android.useFido2Client and the client-level logger id have no effect there.FidoClient configuration.Activity for FIDO calls.UIWindowScene/ASPresentationAnchor for FIDO calls.Full passkey E2E strategy (including OS-level credential surfaces outside app UI) is still to be determined.
This project is licensed under the MIT License - see the LICENSE file for details