Orchestration SDKs

Using device identifiers on React Native

PingOne Advanced Identity Cloud PingAM React Native

The Device Profiling module includes a device identifier in the collected profile. On both Android and iOS, the native SDK uses the same secure identifier generated by the Device ID module.

In React Native, you do not need to call @ping-identity/rn-device-id. The identifier is resolved automatically by the native layer when you call collectDeviceProfile. However, if you need to read the same identifier independently (for logging or display), you can use getDeviceId() from @ping-identity/rn-device-id directly.

Device identifiers serve two distinct roles in the SDK:

Device profiling

A unique identifier is embedded in the device profile payload sent to the server when a DeviceProfileCallback, PingOneProtectInitializeCallback, or PingOneProtectEvaluationCallback is processed.

Device binding

A per-user key identifier (kid) is generated and stored alongside the cryptographic key pair when bindForJourney() registers a device.

Both roles are implemented in the underlying native platform SDKs. The React Native modules delegate identifier generation and storage to those native implementations. Understanding the platform defaults — and knowing when and how to customize them — helps you control identifier stability, sharing across apps, and recovery behavior.

How device identifiers work

The Orchestration SDK for React Native wraps native Android and iOS implementations. Device identifier generation happens entirely in native code; the JavaScript layer passes the identifier transparently as part of the profile or binding payload.

The table provides a platform comparison summary.

Characteristic iOS Android

Default identifier source

Keychain-backed RSA key pair (SHA-256 of public key)

Android KeyStore RSA key pair (SHA-256 of public key)

Persists across app reinstall

Yes

No — regenerated on reinstall

Persists across device restore

Yes (from encrypted backup)

No

Configurable from JavaScript

No — configure in native Xcode project

No — configure in native Android project

Alternate built-in strategy

UUIDDeviceIdentifier

AndroidIDDeviceIdentifier, LegacyDeviceIdentifier

Custom identifier support

Yes — implement DeviceIdentifier protocol

Yes — implement DeviceIdentifier interface

Cross-app sharing

Yes — via Keychain Access Group

Yes — via AndroidIDDeviceIdentifier (API 26+)

iOS defaults

On iOS, the default device identifier is a SHA-256 hash of a public key generated and stored in the Keychain. The Keychain persists across app reinstalls (unless the device is wiped), giving the identifier strong stability.

Default identifier persistence behavior on iOS
Scenario Behavior

App uninstall and reinstall

The identifier persists. The iOS Keychain is not cleared when an app is deleted, so the reinstalled app retrieves the same key.

App data cleared

This is not a standard user action on iOS. App reinstall is the closest equivalent, which preserves the identifier as noted above.

Device backup and restore

The identifier persists if restored from an encrypted iCloud or local backup, because these backups include Keychain data.

Factory reset

The identifier is permanently deleted. A factory reset clears all device storage, including the Keychain.

Sharing across apps (same developer)

The identifier can be shared across apps from the same developer using a Keychain Access Group. Configure the keychainAccount property in the native iOS SDK.

Android defaults

On Android, the default device identifier uses the Android KeyStore to generate and store an RSA key pair. The SHA-256 hash of the public key is the identifier.

Default identifier persistence behavior on Android
Scenario Behavior

App uninstall and reinstall

The identifier is regenerated. Android KeyStore entries are scoped to the app’s UID, which changes on reinstall.

App data cleared (Settings > Clear Data)

The identifier is regenerated. Clearing app data removes the KeyStore entry.

Device backup and restore

The identifier is not restored. Android KeyStore entries are not included in standard backups.

Factory reset

The identifier is permanently deleted.

Sharing across apps

Not supported with the default KeyStore strategy. Use AndroidIDDeviceIdentifier (ANDROID_ID) if cross-app sharing is required on API 26+.

The persistence difference between iOS (persists across reinstalls) and Android (regenerated on reinstall) is important for server-side logic. If your Protect policy or device management rules depend on identifier stability, design for the less stable Android behavior to ensure cross-platform consistency.

Configuring device identifiers

Device identifier behavior in the React Native SDK is controlled by the underlying native platform modules. JavaScript configuration options are limited to concerns that are visible at the JS bridge boundary — primarily logging. Native-level customization (identifier strategy, Keychain account, KeyStore parameters) requires editing the native project files.

Attaching a logger for identifier diagnostics

Both collectDeviceProfile and createBindingClient accept a LoggerInstance.

Attaching a logger gives you visibility into identifier generation and retrieval operations:

import { collectDeviceProfile } from '@ping-identity/rn-device-profile';
import { createBindingClient } from '@ping-identity/rn-binding';
import { logger } from '@ping-identity/rn-logger';

const sdkLogger = logger({ level: 'debug' });

// Device profiling — logger shows native identifier resolution
const profile = await collectDeviceProfile(['platform', 'hardware', 'network'], {
  logger: sdkLogger,
});

// Device binding — logger shows key generation and identifier storage
const bindingClient = createBindingClient({
  logger: sdkLogger,
});

Customizing the default identifier on iOS

The iOS SDK’s DefaultDeviceIdentifier uses a Keychain-backed RSA key pair. To customize its configuration — such as sharing it across apps using a Keychain Access Group, changing the account name, or adjusting the key size — edit the native iOS module initialization in your Xcode project.

Customizing the Keychain account

By default the SDK stores the identifier under a Keychain account derived from your bundle ID.

To share the identifier across apps from the same developer, configure a shared Keychain Access Group:

// In your iOS AppDelegate or a native module initializer
import PingDeviceId

let config = DeviceIdentifierConfiguration(
    keychainAccount: "com.example.shared.deviceid",  // shared across your apps
    useEncryption: true,
    keySize: 2048
)

let deviceIdentifier = try DefaultDeviceIdentifier(configuration: config)

Pass this configured instance to the native SDK before the React Native bridge initializes the modules.

Do not disable encryption (useEncryption: false) in a production app.

Disabling encryption removes the security guarantees of the Keychain-backed identifier.

Regenerating the identifier

To force generation of a new identifier — for example, after a security incident or when a user explicitly requests device removal — call regenerateIdentifier() in native code:

// In a native module exposed to React Native
import PingDeviceId

func resetDeviceIdentifier() async throws -> String {
    let deviceIdentifier = try DefaultDeviceIdentifier()
    let newId = try await deviceIdentifier.regenerateIdentifier()
    print("New Device ID: \(newId)")
    return newId
}

Falling back to UUID-based identifiers

If Keychain operations fail on certain devices (for example, shared enterprise devices with restricted Keychain access), you can fall back to a simpler UUID-based identifier:

import PingDeviceId

// UUIDDeviceIdentifier stores a UUID in UserDefaults rather than the Keychain
let deviceIdentifier = try UUIDDeviceIdentifier()
let id = try await deviceIdentifier.id

UUIDDeviceIdentifier is less stable than the Keychain-backed default: the UUID is cleared when the app’s data is deleted. Use it only when Keychain access is not reliably available.

Creating a custom iOS identifier

For advanced use cases, implement the DeviceIdentifier protocol to supply a completely custom identifier:

import PingDeviceId

struct CustomDeviceIdentifier: DeviceIdentifier {
    var id: String {
        get async throws {
            // Return your custom stable identifier.
            // Ensure persistence — return the same value across app launches.
            return try await loadOrGenerateStableId()
        }
    }
}

Register the custom implementation with the device profile or binding modules before the app starts processing journeys.

Customizing the default identifier on Android

The Android SDK provides three built-in identifier strategies and supports custom implementations.

Built-in Android identifier strategies

Strategy Persistence after reinstall Description

DefaultDeviceIdentifier

Regenerated

Uses Android KeyStore. Most secure. The default when no strategy is specified.

AndroidIDDeviceIdentifier

Persists (API 26+)

Uses Settings.Secure.ANDROID_ID. Persists across reinstalls on Android 8.0+ because the value is scoped to the signing certificate and user account. Resets on factory reset.

LegacyDeviceIdentifier

Varies

Backward-compatible strategy for apps migrating from older SDK versions. Use only when continuity with pre-migration identifiers is required.

Switching to AndroidIDDeviceIdentifier

To use ANDROID_ID as the device identifier — for example, to maintain a stable identifier across reinstalls — configure the native Android module:

// In your Android Application subclass or native module setup
import com.pingidentity.deviceprofile.DefaultDeviceIdentifier
import com.pingidentity.deviceprofile.AndroidIDDeviceIdentifier

// Replace the default strategy with ANDROID_ID
val deviceIdentifier = AndroidIDDeviceIdentifier(context)

ANDROID_ID is unique per user account and signing certificate on Android 8.0 (API 26) and later. On earlier API levels it may not be reliable. Verify your minimum API requirement before adopting this strategy.

Custom Android identifier

To supply a completely custom identifier on Android, implement the DeviceIdentifier interface:

import com.pingidentity.deviceprofile.DeviceIdentifier

class CustomDeviceIdentifier(private val context: Context) : DeviceIdentifier {
    override suspend fun id(): String {
        // Return your custom stable identifier.
        // SHA-256 hash of custom data is recommended for uniformity.
        val customData = "org.example.${Build.SERIAL}"
        return sha256(customData)
    }

    private fun sha256(input: String): String {
        val digest = MessageDigest.getInstance("SHA-256")
        val hash = digest.digest(input.toByteArray(Charsets.UTF_8))
        return hash.joinToString("") { "%02x".format(it) }
    }
}

Register the implementation with the native SDK before the React Native bridge initializes.

Exposing native identifiers to JavaScript

If your application needs the device identifier value in JavaScript — for example, to display it, log it server-side, or include it in a custom API call — use the getDeviceId function exported directly from the @ping-identity/rn-device-id:

import { getDeviceId } from '@ping-identity/rn-device-id';

export async function getDeviceId(): Promise<string> {
  try {
    const deviceId = await getDeviceId();
    // Use deviceId to display, log server-side, or include in a custom API call
  } catch (error) {
    if (error instanceof DeviceIdError) {
      console.error('Failed to retrieve device ID:', error.message);
    }
  }
}
// iOS: MyDeviceIdModule.swift
@objc(MyDeviceIdModule)
class MyDeviceIdModule: NSObject {
  @objc
  func getDeviceId(_ resolve: @escaping RCTPromiseResolveBlock,
                   reject: @escaping RCTPromiseRejectBlock) {
    Task {
      do {
        let identifier = try DefaultDeviceIdentifier()
        let id = try await identifier.id
        resolve(id)
      } catch {
        reject("DEVICE_ID_ERROR", error.localizedDescription, error)
      }
    }
  }
}
// Android: MyDeviceIdModule.kt
class MyDeviceIdModule(reactContext: ReactApplicationContext)
    : ReactContextBaseJavaModule(reactContext) {

  override fun getName() = "MyDeviceIdModule"

  @ReactMethod
  fun getDeviceId(promise: Promise) {
    CoroutineScope(Dispatchers.IO).launch {
      try {
        val id = DefaultDeviceIdentifier.id()
        promise.resolve(id)
      } catch (e: Exception) {
        promise.reject("DEVICE_ID_ERROR", e.message, e)
      }
    }
  }
}