Orchestration SDKs

Integrating PingOne Recognize on iOS

PingOne Advanced Identity Cloud iOS

The PingOne Recognize module abstracts away the underlying biometric enrollment and authentication protocols, so you can add strong, passwordless authentication to your app with minimal code.

During a journey, the server returns a single callback, PingOneRecognizeCallback, and tells your app which ceremony to run using the operationType output field:

  • ENROLL: perform a biometric enrollment ceremony.

  • AUTHENTICATE: perform a biometric authentication ceremony.

The Orchestration SDK reads the operationType and instantiates the matching typed callback, so the callback your app receives in the node’s callback list is not the raw PingOneRecognizeCallback, but one of:

  • PingOneRecognizeEnrollCallback: returned when operationType is ENROLL; call enroll() on it.

  • PingOneRecognizeAuthenticateCallback: returned when operationType is AUTHENTICATE; call authenticate() on it.

Your app never configures the underlying mobile SDK itself. All configuration, such as the service host and API key, arrives in the callback output from the node.

Before you start, make sure your app meets the following requirements:

  • The app declares camera usage in its Info.plist with the NSCameraUsageDescription key; the biometric ceremony needs camera access to capture the user’s face.

  • The app runs the ceremony on the main thread with an active view controller in the hierarchy, so the biometric UI can present over your app.

Also, ensure that you have completed the server-side setup for PingOne Recognize.

Step 1. Preparing the project to download the dependency

The PingRecognize module depends on the PingOne Recognize mobile SDK, which Ping distributes from a private Cloudsmith repository. The PingRecognize module itself is published to the Orchestration SDK for iOS repository, but your package manager cannot resolve its dependency tree, and therefore cannot download the module, until you configure access to that repository.

To configure your project to download the SDK, obtain an API key and add the repository:

  1. When your PingOne Recognize tenant has been provisioned, sign in to the dashboard for your region to obtain an API key for the keyless repositories:

  2. Add the secure repository so your project can download the underlying Keyless module.

    Your package manager downloads the PingOne Recognize mobile SDK from this protected repository when it resolves the PingRecognize dependency you add in Step 2. Installing the module.

    Complete only the option for your chosen package manager:

    CocoaPods
    1. If you are using CocoaPods, add the Keyless spec repository to your Podfile when you create it in Step 2. Installing the module, replacing <your-api-key> with the API key you obtained in the previous step:

      source 'https://dl.cloudsmith.io/<your-api-key>/keyless/partners/cocoapods/index.git'

      Never commit API keys to source control.

      Inject the key into your Podfile from an environment variable, or generate the file in your CI pipeline.

    Swift Package Manager
    1. If you are using the Swift Package Manager, with Xcode closed, in a terminal window run the following commands to map the registry to the keyless scope:

      swift package-registry set --global --scope keyless https://swift.cloudsmith.io/keyless/partners/
    2. Run the following to authenticate with the API key you obtained in the previous step:

      swift package-registry login https://swift.cloudsmith.io/keyless/partners/ --token <your-api-key>

      Never commit API keys to source control.

      The login command stores the key in your macOS keychain, so it is never written to any project file.

Step 2. Installing the module

Add the PingRecognize module to your Orchestration SDK project.

You can use CocoaPods or the Swift Package Manager to add the dependencies to your iOS project.

Add the dependency using CocoaPods

  1. If you do not already have a Podfile, in a terminal window, run the following command in your project directory to create one:

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

    pod 'PingJourney'
    pod 'PingRecognize' // Add-on for PingOne Recognize
  3. If you configured the CocoaPods option in Step 1, run the following command to install the pods:

    pod install

Add the dependency using Swift Package Manager

  1. With your project open in Xcode, select File > Add Package Dependencies.

  2. In the search bar, enter the Orchestration SDK for iOS repository URL: https://github.com/ForgeRock/ping-ios-sdk.

  3. Select the ping-ios-sdk package, and then click Add Package.

  4. In the Choose Package Products modal, ensure that the PingRecognize library is added to your target project.

  5. Click Add Package.

If the PingRecognize product does not appear in the Choose Package Products modal, confirm that you completed Step 1, because the product only appears after you configure the Keyless registry.

  1. Import the module in the files where you handle the callback:

    // Import the Journeys library
    import PingJourney
    
    // Import the PingOne Recognize library
    import PingRecognize

Step 3. Handling the enroll callback

The server triggers an enrollment ceremony by returning a PingOneRecognizeCallback with operationType = "ENROLL".

The Orchestration SDK resolves the callback from the output fields and instantiates PingOneRecognizeEnrollCallback, so your app only needs to identify that callback in the node’s callback list and call enroll() on it:

import PingJourney
import PingRecognize

if let continueNode = node as? ContinueNode {
  for callback in continueNode.callbacks {
    if let callback = callback as? PingOneRecognizeEnrollCallback {
      let result = await callback.enroll()
      switch result {
      case .success:
        // Enrollment successful, proceed to the next step in the journey
        await continueNode.next()
      case .failure(let error):
        // Enrollment failed, handle the error; see Error handling
        print("Enrollment failed: code=\(error.code), message=\(error.message)")
        await continueNode.next()
      }
    }
  }
}

Optionally, set the retrieveSelfie option to true to receive the image captured during the ceremony on the result:

let result = await callback.enroll { config in
  config.retrieveSelfie = true
}

switch result {
  case .success(let enrollSuccess):
    let selfie: CGImage? = enrollSuccess.selfie
    // Enrollment successful, proceed to the next step in the journey
    await continueNode.next()
  case .failure(let error):
    // Enrollment failed, handle the error
    print("Enrollment failed: code=\(error.code), message=\(error.message)")
    await continueNode.next()
}

Step 4. Handling the authenticate callback

The server requests a biometric authentication ceremony by returning a PingOneRecognizeCallback with operationType = "AUTHENTICATE".

The Orchestration SDK resolves the callback from the output fields and instantiates PingOneRecognizeAuthenticateCallback, so to authenticate an enrolled user, your app only needs to identify that callback in the node’s callback list and call authenticate() on it:

import PingJourney
import PingRecognize

if let continueNode = node as? ContinueNode {
  for callback in continueNode.callbacks {
    if let callback = callback as? PingOneRecognizeAuthenticateCallback {
      let result = await callback.authenticate()
      switch result {
        case .success:
          // Authentication successful, proceed to the next step in the journey
          await continueNode.next()
        case .failure(let error):
          // Authentication failed, handle the error; see Error handling
          print("Authentication failed: code=\(error.code), message=\(error.message)")
          await continueNode.next()
      }
    }
  }
}

The callbacks submit their inputs to the journey automatically. Do not call input() manually, or you risk submitting a duplicate result.

On success, the SDK submits the following inputs to the server automatically:

Signed JWT

A JWT signed by the Keyless authentication service attesting that the ceremony completed on the device: the Keyless service issued it after the user’s captured face matched their enrolled biometric template and passed the liveness checks. The Keyless authentication service, not your app, signs the JWT, so the server can verify its signature rather than trust a result claimed by the client. If the node configured transaction data, the signed JWT also carries it, which binds the ceremony to that specific transaction.

Current user ID

The user’s PingOne Recognize identifier.

Client state

An opaque blob used to restore the user’s account on a new device.

Device public signing key

The public part of the key pair the device generated during enrollment.

If the server sends a value in the clientState output field, such as when the node enables the Generate Client State property, and the device is not yet enrolled, then the device is enrolled automatically during the authentication ceremony.

The Orchestration SDK passes the client state to the PingOne Recognize mobile SDK, which uses the client state to restore the user’s account on the device.

The Orchestration SDK handles this fallback itself, so no extra code is needed in your app.

Error handling

All Recognize SDK failures are wrapped in a RecognizeError before they reach your code:

struct RecognizeError: Error, LocalizedError, Sendable {
    let message: String                  // Human-readable description
    let code: Int                        // Numeric code from the underlying Keyless SDK
    let debuggingInfo: [String: String]  // Diagnostic entries, such as the ceremony flow ID, session ID, and stack trace
}

The code and message values are injected into the clientError and clientErrorCode input slots. You must call continueNode.next() after encountering an error so your server receives the error detail, regardless of how your app handles the failed result.

The failure case of the result carries the RecognizeError as its error value, so you can read its code directly, for example:

import PingRecognize

let result = await callback.authenticate()

switch result {
case .success:
  // Authentication successful, proceed to the next step in the journey
  await continueNode.next()
case .failure(let error):
  if let error = error as? RecognizeError {
    switch error.code {
    case 30003: showRetryPrompt("Face scan cancelled")       // User cancelled
    case 30004: showRetryPrompt("Face did not match")        // Face not matching
    case 30005: showRetryPrompt("No network connection")     // No network connection
    case 30007: showLockoutScreen()                          // User lockout
    case 30009: requestCameraPermission()                    // Camera permission denied
    default: logUnexpectedError(error)
    }
  }
  // Continue the journey to report client errors back to the server
  await continueNode.next()
}

The codes in the example are in the user error range of 30000 and above, which result from user behavior during the ceremony.

Codes below 30000 indicate integration or internal errors, such as 20000 when the user is not yet enrolled on the device.