Integrating PingOne Recognize on Android
PingOne Advanced Identity Cloud Android
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.
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 you meet the following requirements:
-
Your journeys run in an activity that extends an AndroidX
ComponentActivity, or another activity implementingActivityResultCaller, so the ceremony can deliver its result to your app. -
You have completed the server-side setup for PingOne Recognize.
Step 1. Preparing the project to download the dependency
The recognize module depends on the PingOne Recognize mobile SDK, which Ping distributes from a private Cloudsmith Maven repository. The recognize module itself is published to Maven Central, but Gradle 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 to your project:
-
When your PingOne Recognize tenant has been provisioned, sign in to the dashboard for your region to obtain an API key for the
keylessrepositories: -
Add the API key to your user-level Gradle properties file, typically at
~/.gradle/gradle.properties:cloudsmithTokenRecognize=<your-api-key>Never commit API keys to source control.
Add the key to your local
gradle.propertiesfile, or inject it as a secret in your CI pipeline. -
In the Project tree view, open the
Gradle Scripts/settings.gradle.ktsfile, and add thekeylesspartner repository. Make sure the variable name in the path matches the property you added in the previous step, and prefix it with a dollar sign, for example,$cloudsmithTokenRecognize:dependencyResolutionManagement { repositories { google() mavenCentral() maven(url = "https://dl.cloudsmith.io/$cloudsmithTokenRecognize/keyless/partners/maven/") } }
Step 2. Installing the module
Add the recognize module to your Orchestration SDK project:
-
In the Project tree view of your Android Studio project, open the
Gradle Scripts/build.gradle.ktsfile for the module. -
In the
dependenciessection, add the required dependencies:dependencies { // Journey module implementation("com.pingidentity.sdks:journey:2.1.0") // PingOne Recognize module implementation("com.pingidentity.sdks:recognize:2.1.0") } -
Synchronize your Gradle project to download the dependencies.
Gradle downloads the recognize module from Maven Central and the PingOne Recognize mobile SDK from the private repository you configured in Step 1. Preparing the project to download the dependency.
|
The callbacks and ceremonies are registered automatically using AndroidX App Startup, so you don’t need to initialize anything in your |
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, so your app only needs to identify the enroll callback and call enroll() on it:
if (node is ContinueNode) {
node.callbacks.forEach { callback ->
if (callback is PingOneRecognizeEnrollCallback) {
val result = callback.enroll()
when {
result.isSuccess -> {
// Enrollment successful, proceed to the next step in the journey
await continueNode.next()
}
result.isFailure -> {
// Enrollment failed, handle the error; see Error handling
val error = result.exceptionOrNull()
println("Enrollment failed: code=${(error as RecognizeException).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:
val result = callback.enroll {
retrieveSelfie = true
}
when {
result.isSuccess -> {
val selfie: Bitmap? = result.getOrNull()?.selfie
// Enrollment successful, proceed to the next step in the journey
await continueNode.next()
}
result.isFailure -> {
val error = result.exceptionOrNull()
println("Enrollment failed: code=${(error as RecognizeException).code}, message=${error.message}")
// Continue the journey to report client errors back to the server
await continueNode.next()
}
}
By default, the Orchestration SDK does not return the captured image.
Step 4. Handling the authenticate callback
The server requests a biometric authentication ceremony by returning a PingOneRecognizeCallback with operationType = "AUTHENTICATE".
To authenticate an enrolled user, call authenticate() on the resolved callback:
if (node is ContinueNode) {
node.callbacks.forEach { callback ->
if (callback is PingOneRecognizeAuthenticateCallback) {
val result = callback.authenticate()
when {
result.isSuccess -> {
// Authentication successful, proceed to the next step in the journey
await continueNode.next()
}
result.isFailure -> {
// Authentication failed, handle the error; see Error handling
val error = result.exceptionOrNull()
println("Authentication failed: code=${(error as RecognizeException).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 signed JWT, current user ID, client state, and freshly retrieved device public signing key to the server automatically.
|
If the server sends a value in the 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 RecognizeException before they reach your code:
class RecognizeException(
val code: Int, // Numeric code from the underlying mobile SDK
override val message: String, // Human-readable description
val debuggingInfo: Map<String, String>, // Diagnostic entries, such as the ceremony flow ID, session ID, and stack trace
cause: Throwable? = null,
) : Exception(message, cause)
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 isFailure branch receives the failed result as a Throwable, so check its type before reading the error detail. PingOne Recognize SDK failures arrive as RecognizeException, but other failures, such as a network error, can arrive as any other exception type, for example:
val result = callback.authenticate()
when {
result.isSuccess -> {
// Authentication successful, proceed to the next step in the journey
await continueNode.next()
}
result.isFailure -> {
val error = result.exceptionOrNull()
when (error) {
is RecognizeException -> {
when (error.code) {
30003 -> showRetryPrompt("Face scan cancelled") // User cancelled
30004 -> showRetryPrompt("Face did not match") // Face not matching
30005 -> showRetryPrompt("No network connection") // No network connection
30007 -> showLockoutScreen() // User lockout
30009 -> requestCameraPermission() // Camera permission denied
else -> println("Other Recognize error: code=${error.code}, message=${error.message}")
}
}
else -> println("Other exception: ${error.message}")
}
// 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, 30000 and above, which result from user behavior during the ceremony, such as cancelling the face scan or failing the biometric match.
Codes below 30000 indicate integration or internal errors, such as 20000 when the user is not yet enrolled on the device.
The final else branch catches any exception that is not a RecognizeException, such as a network error, so your app never crashes on an unexpected failure type.