Orchestration SDKs

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:

Importing the OATH module
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:

Creating the OATH client
import { createOathClient } from '@ping-identity/rn-oath';

const oathClient = await createOathClient();

You can customize the OATH client with a number of options:

Creating a customized OATH client
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

logger

No

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

Records credential operations at debug/info level and errors at error level.

timeout

No

Network timeout, in seconds. Must be greater than or equal to 0.

Default is 15 seconds.

enableCredentialCache

No

Whether to cache credentials in memory for faster repeated reads.

Default is false.

encryptionEnabled

No

(iOS only) Whether to encrypt the credential store in the Keychain.

Default is true.

Do not disable in a production app.

storage

No

An OathStorageHandle for custom storage. Omit to use the platform default; iOS Keychain or Android KeyStore.

policyEvaluator

No

An OathPolicyEvaluatorHandle returned by configureOathPolicyEvaluator().

Enables device health check policy enforcement when generating codes.

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:

Registering an OATH credential from a URI
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

TOTP example
otpauth://totp/Issuer:accountName?secret=BASE32SECRET&issuer=Issuer&algorithm=SHA1&digits=6&period=30
HOTP example
otpauth://hotp/Issuer:accountName?secret=BASE32SECRET&issuer=Issuer&algorithm=SHA1&digits=6&counter=0
OTPAUTH parameter reference
Parameter Required Description

secret

Yes

Base32-encoded shared secret. Stored in the platform secure enclave; never accessible from JavaScript after enrollment.

issuer

Yes

Service name displayed to the user, for example, Example Corp.

algorithm

No

The HMAC algorithm used to compute the one-time passcode.

One of SHA1 (default), SHA256, or SHA512.

digits

No

Code length: 6 (default) or 8.

period

TOTP only

Code validity window, in seconds. Defaults to 30.

counter

HOTP only

Starting counter value. Defaults to 0.

addCredentialFromUri() throws OathError with code OATH_DUPLICATE_CREDENTIAL if a credential with the same issuer and account name already exists.

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();
Retrieving a single OATH credential by ID
// 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:

Updating an OATH credential
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():

  • digits must be 6 or 8.

  • period must be > 0 for TOTP credentials.

  • counter must be >= 0 for HOTP credentials.

Deleting OATH credentials

Call deleteCredential(id) to remove a credential and its stored secret permanently:

Deleting an OATH credential
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:

Generating an HOTP code and submitting it to a journey
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:

Generating a TOTP code and submitting it to a 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

code

string

The current one-time passcode.

timeRemaining

number

Seconds until the code expires.

progress

number

Fractional progress through the validity window (0.0–1.0). 0.0 when fresh, 1.0 when expired.

totalPeriod

number

Total validity window in seconds.

counter

number

Current HOTP counter value. -1 for TOTP credentials.

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

Handling errors from credential operations
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;
    }
  }
}
Handling errors from code generation
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

OATH_CLEANUP_FAILED

iOS

Native cleanup failed internally. Not thrown from close() — iOS always resolves.

OATH_CODE_GENERATION_FAILED

Both

The native SDK could not generate a code for this credential.

OATH_CREDENTIAL_LOCKED

Both

The credential is locked by a device policy; code generation is not allowed.

OATH_CREDENTIAL_NOT_FOUND

Both

No credential with the given ID exists in the native store.

OATH_DUPLICATE_CREDENTIAL

Both

A credential with the same ID already exists in the native store.

OATH_INITIALIZATION_FAILED

Both

The native OATH session could not be created during createOathClient.

OATH_INVALID_PARAMETER

Both

A method argument has an invalid value.

OATH_INVALID_URI

Both

The provided otpauth:// URI could not be parsed.

OATH_MISSING_PARAMETER

iOS

A required method argument was not provided.

OATH_POLICY_VIOLATION

Both

The operation was blocked by a device health check policy.

OATH_STATE_ERROR

Both

A method was called after close(), or the client is in an unexpected internal state.

OATH_STORAGE_ACCESS_DENIED

iOS

The app does not have permission to access the native credential store.

OATH_STORAGE_CORRUPTED

iOS

Stored credential data is corrupted and cannot be read.

OATH_STORAGE_FAILURE

Both

The native credential store encountered an unspecified I/O error.

OATH_UNKNOWN_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:

Closing the OATH client
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 OathError with code OATH_STATE_ERROR. Always check that the OATH client is still active before calling methods from asynchronous callbacks.

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:

Configuring a device health check policy evaluator
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

biometricAvailable

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.

deviceTampering

Code generation fails if the device’s jailbreak or root score exceeds the threshold encoded in the credential’s server-supplied policies field.

The threshold is set server-side and is not a client-side configuration parameter.

configureOathPolicyEvaluator() is synchronous and must be called before createOathClient().

Pass the returned OathPolicyEvaluatorHandle as OathClientConfig.policyEvaluator, as shown above.

Handling device health check policy violations

When a policy blocks code generation, catch OATH_POLICY_VIOLATION and display a contextual message:

Handling a device health check policy violation
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);
    }
  }
}