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, orPingOneProtectEvaluationCallbackis 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 |
|
|
Custom identifier support |
Yes — implement |
Yes — implement |
Cross-app sharing |
Yes — via Keychain Access Group |
Yes — via |
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.
| 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 |
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.
| 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 |
|
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 ( 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 |
|---|---|---|
|
Regenerated |
Uses Android KeyStore. Most secure. The default when no strategy is specified. |
|
Persists (API 26+) |
Uses |
|
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)
|
|
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)
}
}
}
}