Configure a React Native app for OATH MFA
PingOne Advanced Identity Cloud PingAM React Native
This page guides you through configuring OATH-based multi-factor authentication (MFA) in a React Native application using @ping-identity/rn-oath.
Step 1. Installing the module
To install the module into your React Native project, use yarn or npm as follows:
-
yarn
-
npm
yarn add @ping-identity/rn-oath
npm install @ping-identity/rn-oath
After installation, import the functions you might need:
import {
createOathClient,
configureOathPolicyEvaluator,
OathError,
} from '@ping-identity/rn-oath';
import type {
OathClient,
OathClientConfig,
OathCodeInfo,
OathCredential,
OathMfaPolicy,
} from '@ping-identity/rn-oath';
Step 2. Creating the OATH client
To use the OATH module you must initialize the OATH client in your application by calling the createOathClient() method:
import { createOathClient } from '@ping-identity/rn-oath';
const oathClient = await createOathClient();
You can customize the OATH client with a number of options:
import { createOathClient } from '@ping-identity/rn-oath';
import { logger } from '@ping-identity/rn-logger';
const oathLogger = logger({ level: 'debug' });
const oathClient = await createOathClient({
logger: oathLogger,
timeout: 30,
enableCredentialCache: false,
});
The properties you can use to customize OATH client configuration are as follows:
| Option | Required | Description |
|---|---|---|
|
No |
A Records credential operations at Learn more in Configuring logging in React Native. |
|
No |
Network timeout, in seconds. Must be greater than or equal to Default is |
|
No |
Whether to cache credentials in memory for faster repeated reads. Default is |
|
No |
(iOS only) Whether to encrypt the credential store in the Keychain. Default is Do not disable in a production app. |
|
No |
An Learn more in Customizing Journey module storage in React Native. |
|
No |
An Enables device health check policy enforcement when generating codes. Learn more in Enabling OATH device health check policies. |
Step 3. Managing credentials
The OATH module relies on a set of credentials that you can create, retrieve, update, and delete.
Each credential contains the service and account details, and the parameters required to generate HOTP or TOTP codes.
Adding OATH credentials from a URI
The OATH module lets the user register their device for OATH-based multi-factor authentication (MFA).
The information required to register a device is contained in a specially-encoded URI, which your client application decodes to create the credentials.
This URI is often delivered by QR codes that the client can scan, or directly in the callback output by the OATH Registration node.
Use the addCredentialFromUri() method to create OATH credentials and register an MFA device:
const credential = await oathClient.addCredentialFromUri(
'otpauth://totp/Example%3Auser%40example.com?secret=JBSWY3DPEHPK3PXP&issuer=Example&algorithm=SHA1&digits=6&period=30',
);
console.log(credential.id); // opaque stable identifier
console.log(credential.issuer); // "Example"
console.log(credential.accountName); // "user@example.com"
console.log(credential.type); // "TOTP"
The otpauth:// URI format is otpauth://TYPE/LABEL?PARAMETERS
otpauth://totp/Issuer:accountName?secret=BASE32SECRET&issuer=Issuer&algorithm=SHA1&digits=6&period=30
otpauth://hotp/Issuer:accountName?secret=BASE32SECRET&issuer=Issuer&algorithm=SHA1&digits=6&counter=0
| Parameter | Required | Description |
|---|---|---|
|
Yes |
Base32-encoded shared secret. Stored in the platform secure enclave; never accessible from JavaScript after enrollment. |
|
Yes |
Service name displayed to the user, for example, |
|
No |
The HMAC algorithm used to compute the one-time passcode. One of |
|
No |
Code length: |
|
TOTP only |
Code validity window, in seconds. Defaults to |
|
HOTP only |
Starting counter value. Defaults to |
|
Catch this case to prompt the user to replace or keep the existing credential. |
Getting OATH credentials
Call getCredentials() to load all stored credentials, or getCredential(id) to load a single credential by its id:
-
All credentials
-
Single credential
// All credentials
const credentials = await oathClient.getCredentials();
// Single credential
const credential = await oathClient.getCredential('abc123');
if (credential === null) {
console.log('Credential not found');
} else {
console.log('id:', credential.id);
console.log('deviceName:', credential.deviceName);
console.log('uuid:', credential.uuid);
console.log('createdDate:', credential.createdDate);
console.log('lastAccessDate:', credential.lastAccessDate);
}
Updating OATH credentials
Use saveCredential() to persist changes to an existing credential. Retrieve the credential first, modify the desired fields, and save it back:
const credential = await oathClient.getCredential('abc123');
if (credential) {
const updated = await oathClient.saveCredential({
...credential,
displayIssuer: 'My Company',
displayAccountName: 'Work Account',
});
}
Validation rules enforced by saveCredential():
-
digitsmust be6or8. -
periodmust be> 0for TOTP credentials. -
countermust be>= 0for HOTP credentials.
Deleting OATH credentials
Call deleteCredential(id) to remove a credential and its stored secret permanently:
try {
await oathClient.deleteCredential('abc123');
console.log('Credential removed');
} catch (err) {
if (err instanceof OathError) {
if (err.code === 'OATH_CREDENTIAL_NOT_FOUND') {
console.warn('Credential does not exist');
} else {
throw err;
}
}
}
Step 4. Generating OATH-based one-time passcodes
When a journey reaches the OATH Token Verifier node node, it returns a NameCallback expecting a one-time passcode.
Generate the code from the stored credential, populate the callback, then advance the journey:
Generating HOTP codes
HOTP codes are counter-based, so they do not expire.
A typical HOTP flow presents the user with their registered credential and a button to generate a code.
When the user taps the button, call generateCode() and submit the result to the journey.
Each call to generateCode() permanently increments the counter stored on the device. Do not call generateCode() until the user has confirmed they want a code, and submit it to the journey as soon as it is generated, as follows:
import { useJourney, useJourneyForm } from '@ping-identity/rn-journey';
const [node, actions] = useJourney(journeyClient);
const form = useJourneyForm(node);
const code = await oathClient.generateCode(credential.id);
form.setValueByType('NameCallback', code);
if (form.canSubmit) {
await actions.next(form.input);
}
Generating TOTP codes
For TOTP credentials, call generateCodeWithValidity() to get the current code along with its validity window, then submit it to the journey:
import { useJourney, useJourneyForm } from '@ping-identity/rn-journey';
const [node, actions] = useJourney(journeyClient);
const form = useJourneyForm(node);
const info = await oathClient.generateCodeWithValidity(credential.id);
form.setValueByType('NameCallback', info.code);
if (form.canSubmit) {
await actions.next(form.input);
}
In addition to info.code, generateCodeWithValidity() returns timing metadata you can use to drive a countdown display:
| Field | Type | Description |
|---|---|---|
|
|
The current one-time passcode. |
|
|
Seconds until the code expires. |
|
|
Fractional progress through the validity window (0.0–1.0). |
|
|
Total validity window in seconds. |
|
|
Current HOTP counter value. |
Step 5. Handling errors
All OATH operations throw OathError when something goes wrong.
Check err instanceof OathError to confirm the error is OATH-related, then inspect err.code to handle specific failure cases:
-
Credential operations
-
Code generation
import { OathError } from '@ping-identity/rn-oath';
try {
const credential = await oathClient.addCredentialFromUri(uri);
} catch (err) {
if (err instanceof OathError) {
switch (err.code) {
case 'OATH_DUPLICATE_CREDENTIAL':
// Prompt the user to replace or keep the existing credential
break;
case 'OATH_INVALID_URI':
// The scanned QR code is not a valid otpauth:// URI
break;
default:
throw err;
}
}
}
import { OathError } from '@ping-identity/rn-oath';
try {
const info = await oathClient.generateCodeWithValidity(credential.id);
} catch (err) {
if (err instanceof OathError) {
switch (err.code) {
case 'OATH_CREDENTIAL_LOCKED':
// The credential is locked; inform the user
break;
case 'OATH_POLICY_VIOLATION':
// A device health check policy blocked code generation
break;
default:
throw err;
}
}
}
Error codes
| Error code | Platform | Description |
|---|---|---|
|
iOS |
Native cleanup failed internally. Not thrown from |
|
Both |
The native SDK could not generate a code for this credential. |
|
Both |
The credential is locked by a device policy; code generation is not allowed. |
|
Both |
No credential with the given ID exists in the native store. |
|
Both |
A credential with the same ID already exists in the native store. |
|
Both |
The native OATH session could not be created during |
|
Both |
A method argument has an invalid value. |
|
Both |
The provided |
|
iOS |
A required method argument was not provided. |
|
Both |
The operation was blocked by a device health check policy. |
|
Both |
A method was called after |
|
iOS |
The app does not have permission to access the native credential store. |
|
iOS |
Stored credential data is corrupted and cannot be read. |
|
Both |
The native credential store encountered an unspecified I/O error. |
|
Both |
An unexpected error occurred that does not map to a specific code. |
Step 6. Closing the OATH client
Call close() when the OATH client is no longer needed. This releases the native session and its associated memory:
try {
// ... work with credentials
} finally {
await oathClient.close();
}
Close the OATH client when the component is dismissed, for example in a useEffect cleanup function.
|
Calling any method on a closed OATH client throws an |
Enabling OATH device health check policies
The OATH module gates one-time passcode generation behind device health check policies.
When a policy is violated, calling generateCode() or generateCodeWithValidity() throws OathError with code OATH_POLICY_VIOLATION instead of returning a code.
To enable device health check policies, configure a policy evaluator before creating the OATH client:
import {
configureOathPolicyEvaluator,
createOathClient,
} from '@ping-identity/rn-oath';
const deviceHealthEvaluator = configureOathPolicyEvaluator({
policies: [
{ kind: 'biometricAvailable' },
{ kind: 'deviceTampering' },
],
});
const oathClient = await createOathClient({
policyEvaluator: deviceHealthEvaluator,
});
| Policy kind | Description |
|---|---|
|
Code generation fails if the device has no enrolled biometrics. Enforces that the user has set up biometric authentication as a prerequisite for OATH codes. |
|
Code generation fails if the device’s jailbreak or root score exceeds the threshold encoded in the credential’s server-supplied The threshold is set server-side and is not a client-side configuration parameter. |
|
Pass the returned |
Handling device health check policy violations
When a policy blocks code generation, catch OATH_POLICY_VIOLATION and display a contextual message:
import { OathError } from '@ping-identity/rn-oath';
try {
const code = await oathClient.generateCode(credential.id);
} catch (err) {
if (err instanceof OathError) {
if (err.code === 'OATH_POLICY_VIOLATION') {
// Inform the user why the code cannot be generated
setErrorMessage('This code cannot be generated on this device. Enroll biometrics or use an unmodified device.');
} else {
setErrorMessage(err.message);
}
}
}