Orchestration SDKs

Binding keys to a device in iOS

PingOne Advanced Identity Cloud PingAM iOS

The Device Binding module provides secure device registration and authentication capabilities for iOS 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 DeviceBindingCallback when 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 DeviceSigningVerifierCallback when 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

Supported authentication types to access bound keys
  • Biometric Only

  • Biometric with Fallback

  • Application PIN

  • No Authentication

Type name

BIOMETRIC_ONLY

Description

Requires strict biometric authentication with no fallback options

Security level

High

User experience

Streamlined for devices with reliable biometric sensors

Behavior
  • Only accepts biometric authentication, such as a fingerprint, face recognition, or an iris scan

  • Fails immediately if biometric authentication is unavailable or unsuccessful

  • No option to fall back to device PIN, pattern, or password

  • Ideal for high-security applications where biometric verification is mandatory

Use cases

Financial applications, enterprise security, medical applications

Device requirements

Must have functional biometric sensors and enrolled biometric data

Type name

BIOMETRIC_ALLOW_FALLBACK

Description

Prefers biometric authentication but allows fallback to device credentials

Security level

Medium to High

User experience

Flexible with multiple authentication options

Behavior
  • The primary method is a biometric authentication, such as a fingerprint, face recognition, or an iris scan

  • If biometric authentication fails or is unavailable, users can use device credentials

    • Device credentials include a PIN, a pattern, or a password set at the system level

  • Provides better accessibility and usability

Use cases

Consumer applications, general-purpose authentication, accessibility-focused apps

Device requirements
  • Biometric sensors preferred, but not required

  • Must configure the device lock screen

Type name

APPLICATION_PIN

Description

Requires a custom PIN that the application manages entirely

Security level

Medium

User experience

Consistent across all devices regardless of hardware capabilities

Behavior
  • Uses an application-specific PIN separate from device credentials

    • The application collects the PIN through a custom UI

    • The application securely stores PIN data using encrypted storage mechanisms

    • Independent of device biometric capabilities or system-level authentication

Use cases
  • Devices without biometric capabilities

  • Applications requiring custom authentication flows

  • Scenarios where users prefer PIN over biometric authentication

    • Cross-platform consistency requirements

Device requirements

None - works on all devices

Type name

NONE

Description

No user authentication required to access cryptographic keys

Security Level

Low

User Experience

Seamless with no authentication prompts

Behavior
  • Users can access keys immediately without any verification

  • No authentication prompts or delays

  • Cryptographic operations proceed without user interaction

  • Relies solely on device possession for security

Use cases
  • Applications with alternative security measures

Security Considerations

Anyone with device access can use the cryptographic keys

Device Requirements

None

Installing modules

To install the Device Binding module for iOS, use Swift Package Manager (SPM) or Cocoapods to add the dependency to your project.

  • SPM (Swift Package Manager)

  • CocoaPods

You can install packages by using SPM (Swift Package Manager) on the iOS project.

  1. In Xcode, in the Project Navigator, right-click your project, and then click Add Package Dependencies…​.

  2. In the Search or Enter Package URL field, enter the URL of the repo containing the Orchestration SDK for iOS, https://github.com/ForgeRock/ping-ios-sdk.git.

  3. In Add to Project, select the name of your project, and then click Add Package.

    Xcode shows a dialog containing the libraries available for iOS.

  4. Select the PingBinding library, and in the Add to Target column select the name of your project.

  5. Repeat the previous step for any other packages you want to use in your project.

  6. Click Add Package.

    Xcode displays the chosen packages and any prerequisites they might have in the Package Dependencies pane of the Project Navigator.

  1. If you don’t already have CocoaPods, install the latest version.

  2. If you don’t already have a Podfile, in a terminal window, run the following command to create a new Podfile:

    pod init
  3. Add the following lines to your Podfile:

    pod 'PingBinding'
  4. Run the following command to install pods:

    pod install

Binding keys to a device

To bind keys to a device, the Binding Module performs the following tasks:

  1. Validation: Checks device support for authentication type

  2. Cleanup: Removes existing keys for the user

  3. Key generation: Creates new cryptographic key pair

  4. Authentication: Verifies user identity

  5. JWT Signing: Creates signed proof-of-possession

  6. Storage: Saves user key meta data

Use the bind() method to bind keys to the device as follows:

Binding keys to an iOS device
import PingBinding
import PingJourney

private func handleDeviceBinding() {
  Task {
    let result = await callback.bind()
    switch result {
      case .success(let json):
        print("Device binding success: \(json)")
      case .failure(let error):
        if let deviceBindingStatus = error as? DeviceBindingStatus {
          print("Device binding failed: \(deviceBindingStatus.errorMessage)")
        } else {
          print("Device binding failed: \(error.localizedDescription)")
        }
    }
    onNext()
  }
}

Verifying bound keys on a device

To verify that a device possesses a bound key, the Binding Module performs the following tasks:

  1. Validation: Validates custom claims

  2. Key Lookup: Finds appropriate user key

  3. Authentication: Verifies user identity

  4. Challenge signing: Signs server challenge

  5. JWT creation: Creates verification JWT

Use the DeviceSigningVerifierCallback.sign() method to verify possession of bound keys as follows:

Verifying key possession by signing data on an iOS device
import PingBinding
import PingJourney

func handleDeviceSigning(callback: DeviceSigningVerifierCallback, onNext: @escaping () -> Void) {
    Task {
        let result = await callback.sign()
        switch result {
        case .success:
            print("Signing successful")
        case .failure(let error):
            print("Signing failed: \(error.localizedDescription)")
        }
        // Continue to the next node
        onNext()
    }
}

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:

Adding custom claims to the JWT on an iOS device
let result = await callback.sign { config in
  // Use custom claims
  config.claims = [
    "amount": "100.00",
    "recipient": "babs@example.com",
    "currency": "USD",
  ]
}

Configuring authenticator parameters

You can configure a number of device binding parameters, such as the algorithm used, and the validity time.

Configuring AppPinAuthenticator parameters on iOS
import PingBinding

let result = await callback.bind { config in
    // Specify custom app PIN auth parameters
    let appPinConfig = AppPinConfig(
        logger: myCustomLogger,
        prompt: Prompt(title: "Enter PIN", subtitle: "Security", description: "Enter your 4-digit PIN"),
        pinRetry: 5,
        keyTag: "my-custom-key-tag",
        pinCollector: CustomPinCollector()
    )

    // Use the custom authenticator with the config
    config.deviceAuthenticator = AppPinAuthenticator(config: appPinConfig)
}
Configuring Biometric authenticator parameters on iOS
import PingBinding

let result = await callback.bind { config in
    // Specify custom biometric auth parameters
    let biometricConfig = BiometricAuthenticatorConfig(
        logger: myCustomLogger,
        keyTag: "my-biometric-key-tag",
    )

    // Set the authenticator config - uses the appropriate authenticator (BiometricOnlyAuthenticator
    // or BiometricDeviceCredentialAuthenticator) based on the callback type
    config.authenticatorConfig = biometricConfig
}

Replacing the default system UI

The PingBinding module uses system dialogs and alerts by default in the following situations:

Application PIN entry

If the authentication journey specifies the APPLICATION_PIN authentication method to access the secure keys, the module uses a system alert to obtain the PIN from the user.

Key selection

If there are multiple keys available on the device that could be used to fulfil the request, the module uses UIAlertController with an action sheet to display the suitable keys to the user to choose the correct one.

You can implement your own custom user interface for both of these situations.

Creating a custom UI for PIN entry

To implement a custom UI for accepting PIN codes from the user, complete the following steps:

Step 1. Create a Custom UI for PIN Collection

You need to create a view that serves as your PIN entry screen.

The following example uses SwiftUI to create a simple view that collects a 4-digit PIN.

Creating a SwiftUI view to collect a 4-digit PIN
// In your application, e.g., PinCollectorView.swift
import SwiftUI
import PingBinding

struct PinCollectorView: View {
    let prompt: Prompt
    let completion: (String?) -> Void

    @State private var pin: String = ""

    var body: some View {
        VStack(spacing: 20) {
            Text(prompt.title)
                .font(.title)
            Text(prompt.description)
                .font(.subheadline)

            TextField("4-digit PIN", text: $pin)
                .keyboardType(.numberPad)
                .padding()

            HStack {
                Button("Cancel") { completion(nil) }
                Button("Submit") { completion(pin) }
                    .disabled(pin.count != 4)
            }
        }
        .padding()
    }
}

Step 2. Implement the PinCollector Protocol

To allow the PingBinding module to present your custom UI and return the collected PIN code, implement the PinCollector protocol.

Implementing the PinCollector protocol
// In your application, e.g., CustomPinCollector.swift
import UIKit
import SwiftUI
import PingBinding

class CustomPinCollector: PinCollector {
    func collectPin(prompt: Prompt, completion: @escaping @Sendable (String?) -> Void) {
        DispatchQueue.main.async {
            guard let topVC = UIApplication.shared.windows.first?.rootViewController else {
                completion(nil)
                return
            }

            let pinView = PinCollectorView(prompt: prompt) { pin in
                topVC.dismiss(animated: true) {
                    completion(pin)
                }
            }

            let hostingController = UIHostingController(rootView: pinView)
            topVC.present(hostingController, animated: true)
        }
    }
}

Step 3. Use the custom collector for binding and signing

To use your new UI for binding and signing, specify the implementation class in the configuration of the sign() and bind() methods.

Configure binding to use a custom UI for collecting app PINs
// In your view that handles the DeviceBindingCallback
import PingBinding

func handleDeviceBinding(callback: DeviceBindingCallback, onNext: @escaping () -> Void) {
    Task {
        let result = await callback.bind { config in
            // Customize the PIN collector for application PIN authentication
            config.pinCollector = CustomPinCollector()
        }

        // Handle result...
        onNext()
    }
}
Configure signing to use a custom UI for collecting app PINs
// In your view that handles the DeviceSigningVerifierCallback
import PingBinding

func handleDeviceSigning(callback: DeviceSigningVerifierCallback, onNext: @escaping () -> Void) {
    Task {
        let result = await callback.sign { config in
            // Customize the PIN collector for application PIN authentication
            config.pinCollector = CustomPinCollector()
        }

        // Handle result...
        onNext()
    }
}

Creating a custom UI for key selection

Implementing a custom UI for displaying available key pairs to the user to select the correct one is useful in the following situations:

  • Multiple users have bound keys on the same physical device

  • You want to provide a branded UI for key selection

  • You need to display additional context about each key to help with selection

To implement a custom UI for displaying available key pairs, complete the following steps:

. . .

Step 1. Create a custom UI for key selection

You need to create a view that displays the available keys and allow the user to select one.

The following example uses SwiftUI to create a view that allows key selection:

Creating a SwiftUI view to allow key selection
// In your application, e.g., UserKeySelectorView.swift
import SwiftUI
import PingBinding

struct UserKeySelectorView: View {
    let userKeys: [UserKey]
    let prompt: Prompt
    let completion: (UserKey?) -> Void

    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                if !prompt.description.isEmpty {
                    Text(prompt.description)
                        .font(.body)
                        .multilineTextAlignment(.center)
                        .padding()
                }

                List(userKeys, id: \.id) { userKey in
                    Button(action: {
                        completion(userKey)
                    }) {
                        VStack(alignment: .leading, spacing: 4) {
                            if !userKey.username.isEmpty {
                                Text(userKey.username)
                                    .font(.headline)
                            }
                            if !userKey.userId.isEmpty {
                                Text("User ID: \(userKey.userId)")
                                    .font(.caption)
                                    .foregroundColor(.secondary)
                            }
                            Text("Auth: \(userKey.authType.rawValue)")
                                .font(.caption)
                                .foregroundColor(.secondary)
                        }
                        .padding(.vertical, 4)
                    }
                }
            }
            .navigationTitle(prompt.title.isEmpty ? "Select Device Key" : prompt.title)
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button("Cancel") {
                        completion(nil)
                    }
                }
            }
        }
    }
}

Step 2. Implement the UserKeySelector Protocol

To allow the PingBinding module to present your custom UI and return the selected key, implement the UserKeySelector protocol.

Implementing the UserKeySelector protocol
// In your application, e.g., CustomUserKeySelector.swift
import UIKit
import SwiftUI
import PingBinding

class CustomUserKeySelector: UserKeySelector {
    func selectKey(userKeys: [UserKey], prompt: Prompt) async -> UserKey? {
        return await withCheckedContinuation { continuation in
            DispatchQueue.main.async {
                guard let topVC = self.getTopViewController() else {
                    continuation.resume(returning: nil)
                    return
                }

                let selectorView = UserKeySelectorView(
                    userKeys: userKeys,
                    prompt: prompt
                ) { selectedKey in
                    topVC.dismiss(animated: true) {
                        continuation.resume(returning: selectedKey)
                    }
                }

                let hostingController = UIHostingController(rootView: selectorView)
                hostingController.modalPresentationStyle = .formSheet
                topVC.present(hostingController, animated: true)
            }
        }
    }

    private func getTopViewController() -> UIViewController? {
        guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
              let window = windowScene.windows.first(where: { $0.isKeyWindow }),
              let rootViewController = window.rootViewController else {
            return nil
        }

        var topViewController = rootViewController
        while let presentedViewController = topViewController.presentedViewController {
            topViewController = presentedViewController
        }

        return topViewController
    }
}

Step 3. Use the custom key selector for signing

To use your new UI signing, specify the implementation class in the configuration of the sign() and bind() methods.

Configure binding to use a custom UI for selecting keys
import PingBinding

func handleDeviceSigning(callback: DeviceSigningVerifierCallback, onNext: @escaping () -> Void) {
    Task {
        let result = await callback.sign { config in
            // Use custom UI for selecting from multiple device keys
            config.userKeySelector = CustomUserKeySelector()
        }

        switch result {
        case .success:
            print("Signing successful")
        case .failure(let error):
            print("Signing failed: \(error.localizedDescription)")
        }

        onNext()
    }
}

Handling errors

If the Device Binding module returns failure when you call bind() or sign(), you can get the details of the error and take the appropriate action.

Common error codes and how to remediate them
Error Description Remediation

unsupported

The device lacks required capabilities, or the user hasn’t enrolled.

Retry with alternative authentication requirements that don’t require biometrics.

clientNotRegistered

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.

timeout

Operation exceeded timeout.

Allow retry with a longer timeout.

invalidCustomClaims

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 sub, exp, iat, and iss.

Remove or rename the claims listed in the error so they do not clash.

abort

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.

unAuthorize

The user provided invalid credentials.

For example, the user entered an incorrect PIN number.

Allow retry and prompt for the correct credentials.

The following example shows how to handle some of these exceptions:

Handling exceptions when binding keys to a device
let result = await callback.sign()
switch result {
  case .success(let json):
    print("Device signing success: \(json)")
  case .failure(let error):
    if let deviceBindingStatus = error as? DeviceBindingStatus {
      print("Device signing failed: \(deviceBindingStatus.errorMessage)")
    } else {
      print("Device signing failed: \(error.localizedDescription)")
    }
}
onNext()