Bind and verify user devices
The ForgeRock SDKs for Android and iOS can cryptographically bind a mobile device to a user account.
Registered devices generate a key pair and a key ID. The SDK sends the public key and key ID to AM for storage in the user’s profile.
The SDK stores the private key on the device in either the Android KeyStore, or the iOS Secure Enclave. Access to the private keys is protected by either biometric security or a PIN.
A user can bind multiple devices to their account, and each device can bind to multiple users.
After binding a device your authentication journeys in AM can verify ownership of the bound device by requesting that it signs a challenge using the private key.
Relevant authentication nodes and callbacks
The following table covers the authentication nodes and callbacks that AM provides for creating device binding journeys.
Node | Callback | Description |
---|---|---|
Registers a device to the user and optionally stores the public key and key ID in the user’s profile |
||
Non-interactive |
Stores the public key and key ID in the user’s profile if they were stored in node state |
|
Verifies ownership of a device by requesting it signs a challenge and verifying the result |
The SDKs support the default Authentication Type options provided by the authentication nodes. These options define how the user must authenticate on their device to gain access to the private keys stored on it:
Biometric only
-
Request that the client secures access to private keys with biometric security, such as a fingerprint.
Biometric with PIN fallback
-
Request that the client secures access to the private keys with biometric security, such as a fingerprint, but allow use of the device PIN if biometric is unavailable.
Application PIN
-
Request that the client secures access to the private keys with an application-specific PIN.
On Android devices, the private keys used for binding and verification are stored in a keystore file protected by the application PIN specified by the user - it does not use hardware-backed encryption. However, this keystore file is encrypted using keys from the hardware-backed
AndroidKeyStore
.The application-specific PIN applies only to your app, and is not linked to the device PIN used to unlock the device.
The application-specific PIN is stored only on the client device and is not sent to AM.
If the user forgets their application-specific PIN, they must bind the device again.
None
-
The user does not need to authenticate to gain access to the private keys on their device.
The SDKs provide the UI to handle these application types by default. You can also override the default UI and provide your own implementations. Refer to Custom authentication UI.
Add device binding dependencies
To bind a device and perform signing verification, you must add the ForgeRock device binding module to your project.
Add Android dependencies
To add the device binding dependencies to your Android project:
-
In the Project tree view of your Android Studio project, open the
Gradle Scripts/build.gradle
file for the module. -
In the
dependencies
section, add the required dependencies:Exampledependencies
section after editing:dependencies { implementation 'org.forgerock:forgerock-auth:4.2.0' // Device binding core dependencies implementation 'com.nimbusds:nimbus-jose-jwt:9.23' implementation 'androidx.security:security-crypto:1.0.0' // BIOMETRIC_ONLY, BIOMETRIC_WITH_FALLBACK implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha04' // APPLICATION_PIN implementation 'com.madgag.spongycastle:bcpkix-jdk15on:1.58.0.0' }
Add iOS dependencies
You can use CocoaPods or the Swift Package Manager to add the device binding dependencies to your iOS project.
- Add dependencies using CocoaPods
-
-
If you do not already have CocoaPods, install the latest version.
-
If you do not already have a Podfile, in a terminal window run the following command to create a new Podfile:
pod init
-
Add the following lines to your Podfile:
pod 'FRDeviceBinding' // Add-on for Device Binding feature
-
Run the following command to install pods:
pod install
-
- Add dependencies using Swift Package Manager
-
-
With your project open in Xcode, select File > Add Package Dependencies.
-
In the search bar, enter the ForgeRock SDK for iOS repository URL:
https://github.com/ForgeRock/forgerock-ios-sdk
. -
Select the
forgerock-ios-sdk
package, and then click Add Package. -
In the Choose Package Products dialog, ensure that the
FRDeviceBinding
library is added to your target project. -
Click Add Package.
-
In your project, import the library:
// Import the library import FRDeviceBinding
-
Handle device binding callbacks
To bind a device on receipt of a DeviceBindingCallback
, use the DeviceBindingCallback.bind()
function.
This binds the device to the account using the default implementation.
Examples
DeviceBindingCallback callback = node.getCallback(DeviceBindingCallback.class);
callback.setDeviceName("My Android Device");
callback.bind(this.getActivity(), new FRListener<Void>() {
@Override
public void onSuccess(Void result) {
// Proceed to the next node
node.next();
}
@Override
public void onException(Exception e) {
// Proceed to the next node
node.next();
}
});
try {
// Provide a friendly name for the device
callback.setDeviceName("My Android Device")
// Bind the device
callback.bind(context)
// Proceed to the next node
} catch (e: CancellationException) {
// Ignore, due to configuration change
} catch (e: DeviceBindingException) {
// Proceed to the next node
}
// Provide a friendly name for the device
callback.setDeviceName("My iOS Device")
// Bind the device
callback.bind() { result in
switch result {
case .success:
// Proceed to the next node
case .failure(let error):
// Handle the error and proceed to the next node
}
}
The examples above use the default user interface for authenticating users in order to create and securely store private keys. For information on providing your own UI for authenticating access to the private keys, refer to Implement custom UI. |
Handle device signing verifier callbacks
To sign the challenge on receipt of a DeviceSigningVerifierCallback
, use the DeviceBindingCallback.sign()
function.
Examples
callback.sign(requireContext(), new FRListener<Void>() {
@Override
public void onSuccess(Void result) {
// Proceed to the next node
}
@Override
public void onException(Exception e) {
// Proceed to the next node
}
});
try {
callback.sign(context)
// Proceed to the next node
} catch (e: CancellationException) {
// Ignore, due to configuration change
} catch (e: DeviceBindingException) {
// Map custom client errors:
when (e.status) {
is UnRegister -> {
callback.setClientError("UnReg")
}
is UnAuthorize -> {
callback.setClientError("UnAuth")
}
}
// Proceed to the next node
}
callback.sign() { result in
switch result {
case .success:
// Proceed to the next node
case .failure(let error):
// Handle the error and proceed to the next node
}
}
The examples above use the default user interface for authenticating users in order to access the private keys. For information on providing your own UI for authenticating access to the private keys, refer to Implement custom UI. |
Unbind devices by deleting keys
Registered devices store a public key and key ID on the AM server, and the private key in either the Android KeyStore or the iOS Secure Enclave.
To unbind a device from a user the SDK can contact the AM server to delete the keys. When the keys are successfully removed from the server, the SDK can remove the private keys from the device.
Retrieve a list of keys
Call the FRUserKeys().loadAll()
method to obtain a list of keys that are stored on the device:
Delete a key from the server and the device
Call the FRUserKeys().delete(userKey, forceDelete)
method to delete a key from the server.
The parameters are as follows:
userKey
-
Which key to delete.
forceDelete
-
Whether to delete the local key if deleting the key from the server fails.
When set to
true
, the local key is deleted even if removal from the server was not successful.Defaults to
false
, meaning the local key is not deleted if removal from the server fails.
Example:
val frUserKeys = FRUserKeys(context)
frUserKeys.delete(userKey, false)
do {
try FRUserKeys().delete(
userKey: userKey,
forceDelete: false
)
}
catch {
print("Failed to delete public key from server")
}
After deleting keys, the user needs to rebind the device for use in authentication journeys.
Implement custom UI
To ease implementation, the ForgeRock SDKs provide default user interfaces for authenticating in order to access private keys, and also for selecting the private key to use if there is more than one.
Refer to:
Custom authentication UI
When binding a device, or verifying ownership of a device with signing, the user is asked to authorize access to the private keys on their device. The SDKs use a default UI to request this access.
For example, the ForgeRock SDK for Android uses the following default UI:

BIOMETRIC_ONLY
, BIOMETRIC_ALLOW_FALLBACK
, and APPLICATION_PIN
You can override the default UI to implement your own. You can use the same mechanism to override the authentication UI during both binding, and signing.
For example, the following code shows how to implement a custom UI when you configure the binding node to request an APPLICATION_PIN
:
callback.bind(requireContext(), deviceBindingAuthenticationType -> {
switch (deviceBindingAuthenticationType) {
case APPLICATION_PIN: {
return new CustomAppPinDeviceAuthenticator();
}
default:
return callback.getDeviceAuthenticator(deviceBindingAuthenticationType);
}
}, new FRListener<Void>() {
@Override
public void onSuccess(Void result) {
// Proceed to the next node
}
@Override
public void onException(Exception e) {
// Proceed to the next node
}
});
public class CustomAppPinDeviceAuthenticator extends ApplicationPinDeviceAuthenticator {
public CustomAppPinDeviceAuthenticator() {
super((prompt, fragmentActivity, $completion) -> {
$completion.resumeWith("1234".toCharArray());
return IntrinsicsKt.getCOROUTINE_SUSPENDED();
});
}
}
class CustomPinCollector: PinCollector {
override suspend fun collectPin(prompt: Prompt, fragmentActivity: FragmentActivity): CharArray {}
}
class CustomAppPinDeviceAuthenticator: ApplicationPinDeviceAuthenticator(CustomPinCollector())
callback.bind(context) {
when (it) {
// Implement your custom app PIN UI...
APPLICATION_PIN -> CustomAppPinDeviceAuthenticator()
else -> {
callback.getDeviceAuthenticator(it)
}
}
}
callback.bind(deviceAuthenticator: { type in
switch type {
case .applicationPin:
return ApplicationPinDeviceAuthenticator(pinCollector: CustomPinCollector())
default:
return callback.getDeviceAuthenticator(type: type)
}
}, completion: { result in
switch result {
case .success:
// Proceed to the next node
case .failure(let error):
// Handle the error and proceed to the next node
}
})
class CustomPinCollector: PinCollector {
func collectPin(prompt: Prompt, completion: @escaping (String?) -> Void) {
// Implement your custom app PIN UI...
completion("1234")
}
}
Custom key selection UI
When verifying ownership of a device with signing the user might be asked to select which private key to use if they have more than one on their device.

You can override the default key selection UI to implement your own.
callback.sign(requireContext(), new CustomUserKeySelector(), new FRListener<Void>() {
@Override
public void onSuccess(Void result) {
}
@Override
public void onException(Exception e) {
}
});
// Custom user selector that always returns the most recently created key
public class CustomUserKeySelector implements UserKeySelector {
@Nullable
@Override
public Object selectUserKey(@NonNull UserKeys userKeys, @NonNull FragmentActivity fragmentActivity, @NonNull Continuation<? super UserKey> $completion) {
$completion.resumeWith(userKeys.getItems().get(0));
return IntrinsicsKt.getCOROUTINE_SUSPENDED();
}
}
callback.sign(context, CustomUserKeySelector())
// Custom user selector that always returns the most recently created key
class CustomUserKeySelector : UserKeySelector {
override suspend fun selectUserKey(userKeys: UserKeys,
fragmentActivity: FragmentActivity): UserKey {
return userKeys.items[0]
}
}
callback.sign(userKeySelector: CustomUserKeySelector()) { result in
switch result {
case .success:
// Proceed to the next node
case .failure(let error):
// Handle the error and proceed to the next node
}
}
// Custom user selector that always returns the most recently created key
class CustomUserKeySelector: UserKeySelector {
func selectUserKey(userKeys: [UserKey], selectionCallback: @escaping UserKeySelectorCallback) {
selectionCallback(userKeys.first)
}
}
Error handling
If an error occurs when binding a device or signing a secret for verification, the SDK raises an exception. Check the status
property of the exception for information about the problem.
The following table lists the possible values, and the outcomes these map to in the authentication nodes:
Description | Exception status | Mapped node outcome |
---|---|---|
The client device does not support device binding. For example, it does not provide biometric sensors, or the SDK cannot generate the required key pair. |
|
Unsupported |
Binding or signing did not complete before the timeout expired. |
|
Timeout |
The user cancelled binding or signing before completion. |
|
Abort |
The SDK could not locate an existing private key. Either the device has not yet been bound, or the private key was removed. |
|
Unsupported |
The user failed the authentication required to access the private key. For example, they used an unrecognized fingerprint, or the wrong application PIN. |
|
Unsupported |
An unknown, unexpected error occurred. |
|
Abort |
You can map exceptions to custom client error outcomes in the nodes. For example, the following code maps the UnRegister
status to an outcome named CustomUnReg
in the node:
deviceBindingCallback.bind(activity, object : FRListener<Void> {
override fun onSuccess(result: Void?) {
node.next(activity, activity)
}
override fun onException(e: java.lang.Exception?) {
// Custom Error
if (e is DeviceBindingException) {
if (e.status is UnRegister) {
deviceBindingCallback.setClientError("CustomUnReg")
}
}
node.next(activity, activity)
}
})
// Bind the device
callback.bind() { result in
switch result {
case .success:
// Proceed to the next node
case .failure(let error):
// Custom Error
if error == DeviceBindingStatus.unRegister {
callback.setClientError("CustomUnReg")
}
}
}
You must add the name of the custom client error, for example ![]() Figure 3. Custom client error outcome in the device binding node.
|