Binding keys to a device in Android
PingOne Advanced Identity Cloud PingAM Android
The Device Binding module provides secure device registration and authentication capabilities for Android applications.
It enables applications to bind cryptographic keys to a device and restrict access to those keys, using biometrics, a PIN, and other authentication methods.
Before you begin
You need to create an authentication journey in your server using the appropriate nodes to enable device binding.
The nodes you can use for device binding Journeys include the follows:
- Device Binding node
-
Allows users to register one or more devices to their account. A user can bind multiple devices, and each device can be bound to multiple users.
The client receives a
DeviceBindingCallbackwhen reaching this node in a journey. - Device Signing Verifier node
-
Verifies possession of a registered bound device.
The node requires the client device to sign a challenge string using the private key that corresponds to the public key stored on the server during initial binding.
The client receives a
DeviceSigningVerifierCallbackwhen reaching this node in a journey. - Device Binding Storage node
-
Optionally persists collected device binding data to a user’s profile in the identity store.
By default, the Device Binding node stores device data in the user’s profile. You can choose instead to store the device data in transient state, perhaps to run a custom script to extract additional context.
In this case, you can use a Device Binding Storage node to store the data in the user’s profile.
This node runs entirely server-side, and doesn’t send a callback to the client.
Securing access to the keys
The Device Binding module supports four distinct methods for accessing the private key, each offering different levels of security and user experience.
You specify which authentication type your client uses in the configuration of the Device Binding node. To change the authentication type to access the keys, you’ll need to rebind the client device
-
Biometric Only
-
Biometric with Fallback
-
Application PIN
-
No Authentication
| Type name |
|
| Description |
Requires strict biometric authentication with no fallback options |
| Security level |
High |
| User experience |
Streamlined for devices with reliable biometric sensors |
| Behavior |
|
| Use cases |
Financial applications, enterprise security, medical applications |
| Device requirements |
Must have functional biometric sensors and enrolled biometric data |
| Type name |
|
| Description |
Prefers biometric authentication but allows fallback to device credentials |
| Security level |
Medium to High |
| User experience |
Flexible with multiple authentication options |
| Behavior |
|
| Use cases |
Consumer applications, general-purpose authentication, accessibility-focused apps |
| Device requirements |
|
| Type name |
|
| Description |
Requires a custom PIN that the application manages entirely |
| Security level |
Medium |
| User experience |
Consistent across all devices regardless of hardware capabilities |
| Behavior |
|
| Use cases |
|
| Device requirements |
None - works on all devices |
| Type name |
|
| Description |
No user authentication required to access cryptographic keys |
| Security Level |
Low |
| User Experience |
Seamless with no authentication prompts |
| Behavior |
|
| Use cases |
|
| Security Considerations |
Anyone with device access can use the cryptographic keys |
| Device Requirements |
None |
Installing modules
Use the following module in your Android apps to bind keys to a device:
-
binding
You can optionally also install the following modules:
-
binding-ui -
binding-migration -
bcpkix-jdk18on(BouncyCastle open-source cryptographic library)
To install these modules into your Android app:
-
In the Project tree view of your Android Studio project, open the
build.gradle.ktsfile. -
In the
dependenciessection, add thebindingmodule as a dependency:dependencies { implementation("com.pingidentity.sdk:binding:2.0.0") } -
Optionally, you can include the binding-ui dependency, which includes default UI components for the following user interactions:
- Application PIN Collection
-
A Jetpack Compose dialog for secure PIN entry with:
-
Custom PIN input field with masked characters
-
Show/hide PIN visibility toggle
-
Cancel and confirm buttons
-
Error handling and validation feedback
-
- User Key Selection
-
A default UI for multi-user scenarios that displays:
-
List of available user keys with user information
-
Selection interface when multiple users have registered devices
-
User-friendly key identification (username, creation date, etc.)
-
To use the default UI dependency, add it as follows:
dependencies { implementation("com.pingidentity.sdk:binding:2.0.0") implementation("com.pingidentity.sdk:binding-ui:2.0.0") } -
Optionally, you can include the binding-migration dependency, which helps to migrate users with binding keys created by versions of the legacy ForgeRock SDK for Android.
The binding-migration module runs the following steps in the background and requires no additional configuration or user intervention:
-
Detects Legacy Keys: Scans for existing ForgeRock SDK device binding keys and metadata
-
Seamless Migration: Automatically migrates keys to the new SDK format during application startup
-
Preserves User Experience: Users don’t need to re-register their devices after SDK upgrade
-
One-Time Process: Migration occurs once and removes legacy data after successful migration
-
Backward Compatibility: Ensures smooth transition from Legacy SDK without data loss
To use the binding migration dependency, add it as follows:
dependencies { implementation("com.pingidentity.sdk:binding:2.0.0") implementation("com.pingidentity.sdk:binding-ui:2.0.0") implementation("com.pingidentity.sdk:binding-migration:2.0.0") } -
-
Optionally, if you intend to use the Application PIN authentication method to access the private keys, add the following BouncyCastle dependency:
dependencies { implementation("com.pingidentity.sdk:binding:2.0.0") implementation("com.pingidentity.sdk:binding-ui:2.0.0") implementation("com.pingidentity.sdk:binding-migration:2.0.0") implementation("org.bouncycastle:bcpkix-jdk18on:1.82" }
Binding keys to a device
To bind keys to a device, the Binding Module performs the following tasks:
-
Validation: Checks device support for authentication type
-
Cleanup: Removes existing keys for the user
-
Key generation: Creates new cryptographic key pair
-
Authentication: Verifies user identity
-
JWT Signing: Creates signed proof-of-possession
-
Storage: Saves user key meta data
Use the deviceBindingCallback.bind() method to bind keys to the device as follows:
import com.pingidentity.device.binding.journey.DeviceBindingCallback
// Simple device binding with default configuration
val result = deviceBindingCallback.bind()
result.onSuccess { jwt ->
// Device successfully bound, JWT contains proof
println("Device bound successfully: $jwt")
}.onFailure { error ->
// Handle binding failure
println("Binding failed: ${error.message}")
}
Customizing binding parameters
You can configure a number of parameters for binding, such as the device identifier, algorithm used, and the validity time:
val result = deviceBindingCallback.bind {
// Device identification
deviceName = "Babs' Phone"
deviceIdentifier = DefaultDeviceIdentifier.id // Use the default device identifier strategy
// Cryptographic settings
signingAlgorithm = "RS256"
// Timing configuration
issueTime = { Instant.now() }
expirationTime = { timeout -> Instant.now().plusSeconds(timeout.toLong()) }
// Storage configuration
userKeyStorage {
storage {
fileName = "user_keys"
}
}
// Authentication configuration
biometricAuthenticatorConfig {
promptInfo = {
setTitle("Device Registration")
setSubtitle("Secure your account")
setDescription("Use your fingerprint to register this device")
}
}
}
Learn about customizing the device identifier in Customizing device identifiers on Android.
Verifying bound keys on a device
To verify that a device possesses a bound key, the Binding Module performs the following tasks:
-
Validation: Validates custom claims
-
Key Lookup: Finds appropriate user key
-
Authentication: Verifies user identity
-
Challenge signing: Signs server challenge
-
JWT creation: Creates verification JWT
Use the deviceSigningVerifierCallback.sign() method to verify possession of bound keys as follows:
import com.pingidentity.device.binding.journey.DeviceSigningVerifierCallback
// Simple device signing
val result = deviceSigningVerifierCallback.sign()
result.onSuccess { jwt ->
// Challenge successfully signed
println("Challenge signed: $jwt")
}.onFailure { error ->
// Handle signing failure
println("Signing failed: ${error.message}")
}
Customizing signing parameters
You can configure a number of device signing parameters, such as the algorithm used, and the prompts to display:
val result = deviceSigningVerifierCallback.sign {
// Signing algorithm
signingAlgorithm = "RS512"
appPinConfig {
pinRetry = 3
pinCollector {
"1234".toCharArray()
}
prompt = Prompt("App Pin", "Enter your app pin", "App pin is required")
}
// User key selection strategy
userKeySelector { keys ->
// Select most recently created key
keys.maxByOrNull { it.createdAt } ?: keys.first()
}
// Authentication configuration
biometricAuthenticatorConfig {
promptInfo = {
setTitle("Verify Transaction")
setDescription("Confirm this transaction with your fingerprint")
}
}
}
Adding custom claims when signing using bound keys
When signing a server-provided challenge to verify possession of a bound key, you can add custom data to the resulting JSON Web Token (JWT). The server can access and use this data for context, or for auditing purposes.
Add a claims attribute to the configuration, including the key-value pairs you want to add to the JWT:
deviceSigningVerifierCallback.sign {
claims {
// Transaction details
put("amount", "100.00")
put("recipient", "babs@example.com")
put("currency", "USD")
// Device context
put("ip_address", getClientIP())
put("user_agent", getUserAgent())
// Security context
put("risk_score", calculateRiskScore())
put("session_id", getSessionId())
}
}
Handling errors
The Device Binding module can generate several error messages when you call bind() or sign(). Handle these errors to ensure the best possible user experience.
| Error | Description | Remediation |
|---|---|---|
|
The device lacks required capabilities, or the user hasn’t enrolled. |
Retry with alternative authentication requirements that don’t require biometrics. |
|
No keys are available for signing. Either the device hasn’t been registered, or the user has removed the authentication methods that protected the private key. |
Redirect the user to bind a new key to the device. Learn more in Handling key removal by the device. |
|
Operation exceeded timeout. |
Allow retry with a longer timeout. |
|
Reserved claim names used in custom claims parameter. You can’t add custom claims that match the standard required claims in a JWT, such as |
Remove or rename the claims listed in the error so they do not clash. |
|
The user aborted the operation. For example the user clicked Cancel rather than provide their fingerprint. |
Handle gracefully, and don’t show error. The user chose not to continue the authentication flow. |
|
Biometric authentication failed. |
Retry biometric authentication, or offer an alternative authentication method that doesn’t require biometrics. |
|
The user provided invalid credentials. For example, the user entered an incorrect PIN number. |
Allow retry and prompt for the correct credentials. |
|
Coroutine operation cancelled. |
Re-throw the exception to preserve cancellation semantics. |
|
User binds keys to their device with |
User must perform device binding again to generate new keys. Learn more in Handling biometric enrollment invalidations. |
The following example shows how to handle some of these exceptions:
deviceBindingCallback.bind().fold(
onSuccess = { jwt ->
// Handle success
processBindingSuccess(jwt)
},
onFailure = { error ->
when (error) {
is DeviceNotSupportedException -> {
logger.w("Device not supported: ${error.message}")
showFallbackAuthentication()
}
is TimeoutCancellationException -> {
logger.w("Binding operation timed out")
retryWithLongerTimeout()
}
else -> {
logger.e("Binding failed", error)
showGenericError(error.message)
}
}
}
)
Handling biometric enrollment invalidations
Setting the setInvalidatedByBiometricEnrollment parameter to true when binding a new key to a device invalidates the key if the user enrolls a new fingerprint or changes the registered biometrics on the device. The Device Binding module returns KeyPermanentlyInvalidatedException in this case.
If the authentication method for signing is set to BIOMETRIC_ONLY the invalidated keys won’t be available, so the user will need to bind a new key:
KeyPermanentlyInvalidatedException exceptions// Configuration that makes keys sensitive to biometric changes
biometricAuthenticatorConfig {
keyGenParameterSpec {
// This setting makes keys invalid when new biometrics are enrolled
setInvalidatedByBiometricEnrollment(true)
//setUnlockedDeviceRequired(true)
//setUserAuthenticationValidWhileOnBody(true)
//setUserPresenceRequired(true)
//setIsStrongBoxBacked(false)
//setInvalidatedByBiometricEnrollment(false)
}
}
// When user enrolls new fingerprint, subsequent signing will fail
deviceSigningCallback.sign().onFailure { error ->
when (error) {
is KeyPermanentlyInvalidatedException -> {
// Keys are permanently invalidated due to biometric enrollment
logger.w("Device keys invalidated by biometric enrollment")
redirectToDeviceBinding() // User must re-bind device
}
}
}
|
Not all Android devices support the You should test the devices you want to support if you enable them |
Handling key removal by the device
If a user disables all of the available authentication methods on their device, such as removing their fingerprints and the device PIN, Android automatically removes any keys protected by those methods from the KeyStore.
The Device Binding module returns DeviceNotRegisteredException in this case, and the user will need to bind a new key to their device:
DeviceNotRegisteredException exceptions// When all device authentication is removed, keys are deleted by Android KeyStore
deviceSigningCallback.sign().onFailure { error ->
when (error) {
is DeviceNotRegisteredException -> {
logger.w("No device keys found - maybe removed due to authentication method removal")
showMessage("Please enroll in biometrics or add a device PIN, then register your device")
redirectToDeviceBinding() // User must re-bind device
}
}
}