Following the acquisition, Onfido is now known as Entrust.Read more
Onfido LogoOnfido Logo

Developers

Biometric Passkey: SDK integration

Introduction

This guide shows mobile integrators how to embed the Biometric Passkey SDK in Android and iOS applications. The SDK creates device-bound passkeys, runs Entrust Identity Verification when a flow requires it, signs WebAuthn assertions, and returns WebAuthn artifacts to your app. Your app forwards those artifacts to your backend, and your backend completes the corresponding flow described in Biometric Passkey: Core API integration.

Please note: The Biometric Passkey SDK and API are exclusively for the management of biometric passkey credentials and are distinct and separate from the Entrust IDV SDKs and API. For integrating the Entrust IDV SDKs for identity verification, please refer to our documentation here.

Requirements

  • android logoAndroid
  • ios logoiOS
  • RequirementNotes
    Android versionAndroid 9 (API level 28) or later.
    Compile SDKAPI level 37.
    Language and runtimeKotlin with Java/JVM target 17. SDK operations are suspend functions.
    Host activityUse a FragmentActivity or AppCompatActivity host for flows that show Entrust Identity Verification or biometric UI.
    NetworkingserverBaseUrl must use HTTPS for remote hosts. HTTP is accepted only for localhost or 127.0.0.1 during local development.
    Optional provider integrationAndroid Credential Provider requires Android 14 (API level 34) or later.

    Your app must declare the permissions required by the journeys you enable, such as camera access for identity verification. The SDK declares network access, but your app remains responsible for requesting runtime camera permission before starting flows that need camera capture.

    RequirementNotes
    iOS versioniOS 17.0 or later.
    Language and runtimeSwift with Swift Concurrency (async / await).
    FrameworksLocalAuthentication, AuthenticationServices, Keychain, and Secure Enclave-backed passkey operations.
    NetworkingserverBaseUrl must use HTTPS for remote hosts. HTTP is accepted only for localhost or 127.0.0.1 during local development.
    Keychain access groupRequired to scope SDK credential metadata and passkey references. Use a shared access group if you add a Credential Provider Extension.
    Optional provider integrationiOS AutoFill requires a Credential Provider Extension target and associated entitlements.

    Your app must include the usage descriptions required by the journeys you enable, including camera and Face ID usage descriptions for identity verification and passkey signing.

    Install the SDK

  • android logoAndroid
  • ios logoiOS
  • The Android SDK is published to Maven Central. Use the version shipped with your release:

    kotlin
    1// app/build.gradle.kts
    2dependencies {
    3 implementation("com.entrust.identity.biometricpasskey:android-sdk:<version>")
    4}

    Replace <version> with the Android SDK version from your SDK release notes or package manager channel. The Maven coordinate is com.entrust.identity.biometricpasskey:android-sdk; Kotlin imports use the SDK package namespace com.entrust.identity.biometricpasskey.sdk.... See Biometric Passkey: Version policy for supported release lines and backend compatibility guidance.

    Use standard Android repositories:

    kotlin
    1// settings.gradle.kts
    2dependencyResolutionManagement {
    3 repositories {
    4 google()
    5 mavenCentral()
    6 }
    7}

    The iOS SDK is distributed through the public GitHub Swift package repository EntrustCorporation/biometric-passkey-ios. The package exposes the BiometricPasskeySDK product and downloads a versioned XCFramework asset from that repository's release assets.

    swift
    1// Package.swift
    2dependencies: [
    3 .package(url: "https://github.com/EntrustCorporation/biometric-passkey-ios", from: "<version>")
    4]
    5
    6// In your target:
    7.target(
    8 name: "YourApp",
    9 dependencies: [
    10 .product(name: "BiometricPasskeySDK", package: "biometric-passkey-ios")
    11 ]
    12)

    Replace <version> with the supported semantic tag from your iOS SDK release notes. See Biometric Passkey: Version policy for supported release lines and backend compatibility guidance.

    If you integrate through Xcode, add the same package URL with File > Add Package Dependencies.

    After adding the package, configure your Xcode project:

    1. Add NSCameraUsageDescription to your app's Info.plist (required for identity verification capture).
    2. Add NSFaceIDUsageDescription to your app's Info.plist (required for the LocalAuthentication prompts used by signing and provider flows).
    3. Enable the Keychain Sharing capability and add your Keychain access group.
    4. If you use a Credential Provider Extension, add the AutoFill Credential Provider capability to the extension target.
    5. Add an Associated Domains entitlement with webcredentials:<your-rp-domain> if your relying party requires domain validation.

    The SDK requires a physical device with Secure Enclave for passkey operations. Simulator builds compile but fail at runtime when generating or signing with device-bound keys.

    Quick start

    The following shows the minimal path from SDK setup through enrollment and first authentication. Full details for each step are in the sections below.

    All backend.* calls in these examples represent your own app's network layer — they are not part of the SDK.

    Enroll

  • android logoAndroid
  • ios logoiOS
  • kotlin
    1// 1. Build configuration
    2val configuration = BiometricPasskeyConfiguration(
    3 rpDomain = "auth.example.com",
    4 serverBaseUrl = "https://api.example.com",
    5)
    6
    7// 2. Create the SDK instance in Activity.onCreate
    8val biometricPasskey: BiometricPasskeyClient = BiometricPasskeyClient.create(this, configuration)
    9
    10// 3. Enroll
    11val start = backend.startRegistration(currentUserId)
    12val output = biometricPasskey.enroll(
    13 EnrollmentInput(
    14 challenge = start.challenge,
    15 userId = start.userId,
    16 workflowRunId = start.workflowRunId,
    17 idvToken = start.idvToken,
    18 expiresAt = start.expiresAt,
    19 ),
    20)
    21backend.completeRegistration(
    22 registrationAttemptId = start.registrationAttemptId,
    23 credentialId = output.credentialId,
    24 attestationObject = output.attestationObject,
    25 clientDataJson = output.clientDataJson,
    26)
    swift
    1// 1. Build configuration
    2let configuration = try BiometricPasskeyConfiguration(
    3 rpDomain: "auth.example.com",
    4 serverBaseUrl: URL(string: "https://api.example.com")!,
    5 keychainAccessGroup: "TEAMID.com.example.passkeys"
    6)
    7
    8// 2. Create the SDK instance
    9let biometricPasskey: BiometricPasskeyClientProtocol = BiometricPasskeyClient(configuration: configuration)
    10
    11// 3. Enroll
    12let start = try await backend.startRegistration(userId: currentUserId)
    13let output = try await biometricPasskey.enroll(
    14 EnrollmentInput(
    15 challenge: start.challenge,
    16 userId: start.userId,
    17 workflowRunId: start.workflowRunId,
    18 idvToken: start.idvToken,
    19 expiresAt: start.expiresAt
    20 )
    21)
    22try await backend.completeRegistration(
    23 registrationAttemptId: start.registrationAttemptId,
    24 credentialId: output.credentialId,
    25 attestationObject: output.attestationObject,
    26 clientDataJson: output.clientDataJson
    27)

    Authenticate

  • android logoAndroid
  • ios logoiOS
  • kotlin
    1val start = backend.startAuthentication(currentUserId)
    2val output = biometricPasskey.authenticate(
    3 AuthenticationInput(
    4 challenge = start.challenge,
    5 allowCredentials = start.allowCredentials,
    6 biometricPasskeySessionId = start.biometricPasskeySessionId,
    7 ),
    8)
    9backend.completeAuthentication(
    10 biometricPasskeySessionId = output.biometricPasskeySessionId,
    11 credentialId = output.credentialId,
    12 authenticatorData = output.authenticatorData,
    13 clientDataJson = output.clientDataJson,
    14 signature = output.signature,
    15)
    swift
    1let start = try await backend.startAuthentication(userId: currentUserId)
    2let output = try await biometricPasskey.authenticate(
    3 AuthenticationInput(
    4 challenge: start.challenge,
    5 allowCredentials: start.allowCredentials,
    6 biometricPasskeySessionId: start.biometricPasskeySessionId
    7 )
    8)
    9try await backend.completeAuthentication(
    10 biometricPasskeySessionId: output.biometricPasskeySessionId,
    11 credentialId: output.credentialId,
    12 authenticatorData: output.authenticatorData,
    13 clientDataJson: output.clientDataJson,
    14 signature: output.signature
    15)

    Configure your app

    The configuration ties local passkey operations to your relying-party domain and to the mobile backend routes the SDK can call for identity verification proxy operations.

  • android logoAndroid
  • ios logoiOS
  • kotlin
    1import com.entrust.identity.biometricpasskey.sdk.api.BiometricPasskeyConfiguration
    2import com.entrust.identity.biometricpasskey.sdk.api.BiometricPasskeyStorageConfiguration
    3
    4val configuration = BiometricPasskeyConfiguration(
    5 rpDomain = "auth.example.com",
    6 serverBaseUrl = "https://api.example.com",
    7 storageConfiguration = BiometricPasskeyStorageConfiguration(
    8 preferencesNamespace = "example_prod",
    9 ),
    10)
    SettingDetails
    rpDomainFully qualified relying-party domain used for WebAuthn credential operations. It must match the relying-party ID your backend uses.
    serverBaseUrlHTTPS base URL for your mobile backend surface. The SDK appends its mobile proxy paths to this URL.
    preferencesNamespaceOptional local storage namespace. Use a stable namespace per app flavor or tenant when local credential data must be isolated. It must be 1-64 characters and use only letters, digits, ., _, or -.

    Configuration validation runs when BiometricPasskeyConfiguration is constructed and throws BiometricPasskeyException with INVALID_CONFIGURATION for invalid values.

    Common mistakes: an rpDomain that doesn't exactly match your backend's relying-party ID causes WebAuthn assertion failures at runtime; a non-localhost serverBaseUrl that uses http:// instead of https:// causes the SDK to reject the configuration immediately.

    swift
    1import BiometricPasskeySDK
    2
    3let configuration = try BiometricPasskeyConfiguration(
    4 rpDomain: "auth.example.com",
    5 serverBaseUrl: URL(string: "https://api.example.com")!,
    6 keychainAccessGroup: "TEAMID.com.example.passkeys"
    7)
    SettingDetails
    rpDomainFully qualified relying-party domain used for WebAuthn credential operations. It must match the relying-party ID your backend uses.
    serverBaseUrlHTTPS base URL for your mobile backend surface. The SDK appends its mobile proxy paths to this URL.
    keychainAccessGroupKeychain access group used for credential metadata and passkey references. Use the same access group in the host app and Credential Provider Extension when AutoFill is enabled. Format: $(TeamID).com.example.shared.

    Configuration validation runs in BiometricPasskeyConfiguration(...) and throws BiometricPasskeyError with .invalidConfiguration for invalid values. The BiometricPasskeyClient(configuration:) initializer itself does not throw. SDK-owned mobile proxy paths are fixed; see Backend contract for SDK calls.

    Common mistakes: a missing webcredentials:<rp-domain> entry in your Associated Domains entitlement causes WebAuthn operations to fail silently at runtime; a keychain access group that differs between the host app and the Credential Provider Extension breaks AutoFill because the extension cannot read host-app enrolled credentials — use a shared access group with the format $(TeamID).your.shared.group.

    Initialize the SDK

  • android logoAndroid
  • ios logoiOS
  • Create the SDK from an activity before flows that register activity-result callbacks can start. The recommended location is the host activity's onCreate before onStart.

    kotlin
    1import android.os.Bundle
    2import androidx.fragment.app.FragmentActivity
    3import com.entrust.identity.biometricpasskey.sdk.api.BiometricPasskeyClient
    4
    5class MainActivity : FragmentActivity() {
    6 private lateinit var biometricPasskey: BiometricPasskeyClient
    7
    8 override fun onCreate(savedInstanceState: Bundle?) {
    9 super.onCreate(savedInstanceState)
    10
    11 biometricPasskey = BiometricPasskeyClient.create(
    12 context = this,
    13 configuration = configuration,
    14 )
    15 }
    16}

    Keep the SDK instance in your activity, coordinator, or dependency graph for the active app session. Flows that show identity verification or biometric UI require an active compatible activity.

    Create the SDK once in your app dependency graph and inject BiometricPasskeyClientProtocol into features that need it.

    swift
    1import BiometricPasskeySDK
    2import SwiftUI
    3
    4@main
    5struct ExampleApp: App {
    6 let biometricPasskey: BiometricPasskeyClientProtocol
    7
    8 init() {
    9 let configuration: BiometricPasskeyConfiguration
    10 do {
    11 configuration = try BiometricPasskeyConfiguration(
    12 rpDomain: "auth.example.com",
    13 serverBaseUrl: URL(string: "https://api.example.com")!,
    14 keychainAccessGroup: "TEAMID.com.example.passkeys"
    15 )
    16 } catch {
    17 fatalError("Invalid SDK configuration: \(error)")
    18 }
    19 biometricPasskey = BiometricPasskeyClient(configuration: configuration)
    20 }
    21
    22 var body: some Scene {
    23 WindowGroup {
    24 RootView()
    25 }
    26 }
    27}

    Replace the fatalError with your app's configuration-error handling in production paths.

    Backend contract for SDK calls

    Your mobile app and backend need a small orchestration contract around the SDK. The SDK methods consume input bundles that your backend creates, and your backend consumes output bundles that the SDK returns.

    FlowApp obtains before SDK callSDK returns to appBackend completes after SDK call
    EnrollmentRegistration attempt identifier, WebAuthn challenge, identity-verification token, workflow run identifier, expiry, user identifier, and optional user handle.Credential ID, attestation object, and client data JSON.Registration completion and finalization.
    In-app step-upAuthentication session identifier, WebAuthn challenge, allow-list credential IDs, and user identifier.Credential ID, authenticator data, client data JSON, signature, session identifier, and optional user handle.Authentication-session completion and high-risk action decision.
    Cross-platform step-upResumed cross-platform session context, authentication session identifier, WebAuthn challenge, allow-list credential IDs, and user identifier.Same assertion output as in-app step-up.Cross-platform authentication completion and browser-side status transition.
    RecoveryRecovery attempt identifier, recovery token, and recovery attempt expiry. The SDK later asks your app for a replacement registration bundle through a callback.Replacement credential ID, attestation object, and client data JSON.Recovery registration completion and finalization.

    The SDK also calls your mobile backend surface for identity-verification start operations. These paths are fixed in the SDK and are resolved relative to serverBaseUrl. Your backend must expose them at exactly these paths:

    PurposeMobile proxy path
    In-app step-up identity verification/biometric-passkey/mobile/step-up/{biometricPasskeySessionId}/idv/start
    Cross-platform step-up identity verification/biometric-passkey/mobile/cross-platform/{crossPlatformSessionId}/idv/start
    Recovery identity verification/biometric-passkey/mobile/recovery/{recoveryAttemptId}/idv/start

    These are your backend proxy routes. They should call the corresponding Biometric Passkey API operations and return only the SDK-safe identity verification payload expected by the mobile SDK. Android and iOS SDK models use unpadded base64url strings for WebAuthn challenges, user handles, attestation objects, authenticator data, signatures, and client data JSON unless a field explicitly says otherwise. Credential identifiers are opaque SDK strings; store and forward them exactly as returned, and do not assume the enrollment credentialId string and later assertion credentialId string have identical platform encoding on every SDK.

    Enroll a Biometric Passkey

    Enrollment creates a new device-bound passkey after a successful identity verification capture.

  • android logoAndroid
  • ios logoiOS
  • kotlin
    1import com.entrust.identity.biometricpasskey.sdk.api.model.EnrollmentInput
    2
    3suspend fun enrollPasskey(start: RegistrationStartBundle) {
    4 val output = biometricPasskey.enroll(
    5 EnrollmentInput(
    6 challenge = start.challenge,
    7 userId = start.userId,
    8 workflowRunId = start.workflowRunId,
    9 idvToken = start.idvToken,
    10 expiresAt = start.expiresAt,
    11 userHandle = start.userHandle,
    12 ),
    13 )
    14
    15 backend.completeRegistration(
    16 registrationAttemptId = start.registrationAttemptId,
    17 credentialId = output.credentialId,
    18 attestationObject = output.attestationObject,
    19 clientDataJson = output.clientDataJson,
    20 )
    21}

    All WebAuthn binary fields (challenge, userHandle, attestationObject, clientDataJson) are exchanged as base64url-encoded strings. Pass the challenge exactly as your backend returns it without re-encoding. If userHandle is omitted, the SDK derives a stable handle from userId.

    swift
    1import BiometricPasskeySDK
    2
    3func enrollPasskey(_ start: RegistrationStartBundle) async throws {
    4 let output = try await biometricPasskey.enroll(
    5 EnrollmentInput(
    6 challenge: start.challenge,
    7 workflowRunId: start.workflowRunId,
    8 idvToken: start.idvToken,
    9 expiresAt: start.expiresAt,
    10 userId: start.userId,
    11 userHandle: start.userHandle
    12 )
    13 )
    14
    15 try await backend.completeRegistration(
    16 registrationAttemptId: start.registrationAttemptId,
    17 credentialId: output.credentialId,
    18 attestationObject: output.attestationObject,
    19 clientDataJson: output.clientDataJson
    20 )
    21}

    The iOS SDK exchanges all WebAuthn binary fields (challenge, userHandle, attestationObject, clientDataJson) as base64url-encoded strings, matching standard WebAuthn transport encoding. Pass the challenge exactly as your backend returns it without re-decoding. If userHandle is omitted, the SDK derives a stable handle from userId.

    Authenticate with a Biometric Passkey

    Use authenticate when your backend has issued an authentication challenge and your app needs the SDK to sign a WebAuthn assertion. The same method supports SDK-managed standard local authentication, in-app step-up, and cross-platform step-up. If your app uses platform passkey UI for ordinary sign-in, keep that flow separate and use authenticate for SDK-managed assertions and step-up. For step-up sessions, pass the UUID-formatted biometricPasskeySessionId issued by the Core API; the SDK treats non-UUID session IDs as standard local SDK-managed authentication and does not start IDV. For step-up sessions, the SDK starts the required Entrust Identity Verification flow through your mobile proxy before producing the assertion.

  • android logoAndroid
  • ios logoiOS
  • kotlin
    1import com.entrust.identity.biometricpasskey.sdk.api.model.AuthenticationInput
    2
    3suspend fun approveHighRiskAction(start: AuthenticationStartBundle) {
    4 val output = biometricPasskey.authenticate(
    5 AuthenticationInput(
    6 challenge = start.challenge,
    7 allowCredentials = start.allowCredentials,
    8 biometricPasskeySessionId = start.biometricPasskeySessionId,
    9 crossPlatformSessionId = start.crossPlatformSessionId,
    10 ),
    11 )
    12
    13 backend.completeAuthentication(
    14 biometricPasskeySessionId = output.biometricPasskeySessionId,
    15 credentialId = output.credentialId,
    16 authenticatorData = output.authenticatorData,
    17 clientDataJson = output.clientDataJson,
    18 signature = output.signature,
    19 userHandle = output.userHandle,
    20 )
    21}

    If no local credential matches the allow-list, branch on PASSKEY_NOT_FOUND and offer account recovery or another customer-approved fallback. All output binary fields (authenticatorData, clientDataJson, signature, userHandle) are base64url-encoded strings — forward them to your backend unchanged.

    swift
    1import BiometricPasskeySDK
    2
    3func approveHighRiskAction(_ start: AuthenticationStartBundle) async throws {
    4 let output = try await biometricPasskey.authenticate(
    5 AuthenticationInput(
    6 challenge: start.challenge,
    7 allowCredentials: start.allowCredentials,
    8 biometricPasskeySessionId: start.biometricPasskeySessionId,
    9 crossPlatformSessionId: start.crossPlatformSessionId
    10 )
    11 )
    12
    13 try await backend.completeAuthentication(
    14 biometricPasskeySessionId: output.biometricPasskeySessionId,
    15 credentialId: output.credentialId,
    16 authenticatorData: output.authenticatorData,
    17 clientDataJson: output.clientDataJson,
    18 signature: output.signature,
    19 userHandle: output.userHandle
    20 )
    21}

    If no local credential matches the allow-list, branch on .passkeyNotFound and offer account recovery or another customer-approved fallback. All output binary fields (authenticatorData, clientDataJson, signature, userHandle) are base64url-encoded strings — forward them to your backend unchanged.

    allowCredentials is a list of base64url-encoded credential IDs that your backend returns in the authentication bundle. The SDK only considers credentials in that list when selecting a key for signing. The list must not be empty — both SDKs throw an invalidConfiguration error immediately if an empty list is passed.

    Handle cross-platform step-up

    Cross-platform step-up lets a user confirm a browser-initiated action in the mobile app. Your backend creates the cross-platform session and chooses how to deliver the handoff token to the mobile device. Common delivery channels are push notifications, QR codes, deep links, and Universal Links.

    The mobile sequence is:

    1. Receive the handoff token through your chosen channel.
    2. Send the token to your backend to resume the cross-platform session.
    3. Ask your backend for the mobile authentication bundle for that resumed session.
    4. Call authenticate with both biometricPasskeySessionId and crossPlatformSessionId.
    5. Forward the SDK assertion output to your backend.

    Your backend can combine the resume and authentication-bundle steps into one mobile endpoint if that endpoint returns the same SDK input fields.

  • android logoAndroid
  • ios logoiOS
  • kotlin
    1suspend fun handleCrossPlatformHandoff(handoffToken: String) {
    2 val resumed = backend.resumeCrossPlatformSession(handoffToken)
    3 val start = backend.startCrossPlatformAuthentication(
    4 crossPlatformSessionId = resumed.crossPlatformSessionId,
    5 )
    6
    7 val output = biometricPasskey.authenticate(
    8 AuthenticationInput(
    9 challenge = start.challenge,
    10 allowCredentials = start.allowCredentials,
    11 biometricPasskeySessionId = start.biometricPasskeySessionId,
    12 crossPlatformSessionId = resumed.crossPlatformSessionId,
    13 ),
    14 )
    15
    16 backend.completeAuthentication(
    17 biometricPasskeySessionId = output.biometricPasskeySessionId,
    18 credentialId = output.credentialId,
    19 authenticatorData = output.authenticatorData,
    20 clientDataJson = output.clientDataJson,
    21 signature = output.signature,
    22 userHandle = output.userHandle,
    23 )
    24}
    swift
    1func handleCrossPlatformHandoff(_ handoffToken: String) async throws {
    2 let resumed = try await backend.resumeCrossPlatformSession(handoffToken)
    3 let start = try await backend.startCrossPlatformAuthentication(
    4 crossPlatformSessionId: resumed.crossPlatformSessionId
    5 )
    6
    7 let output = try await biometricPasskey.authenticate(
    8 AuthenticationInput(
    9 challenge: start.challenge,
    10 allowCredentials: start.allowCredentials,
    11 biometricPasskeySessionId: start.biometricPasskeySessionId,
    12 crossPlatformSessionId: resumed.crossPlatformSessionId
    13 )
    14 )
    15
    16 try await backend.completeAuthentication(
    17 biometricPasskeySessionId: output.biometricPasskeySessionId,
    18 credentialId: output.credentialId,
    19 authenticatorData: output.authenticatorData,
    20 clientDataJson: output.clientDataJson,
    21 signature: output.signature,
    22 userHandle: output.userHandle
    23 )
    24}

    Recover an account

    Recovery is a mobile app flow for a replacement device that does not have the user's existing passkey. The SDK exposes one recovery entry point. It verifies the user through Entrust Identity Verification, asks your app for a replacement registration bundle through RecoveryRegistrationProvider, creates the replacement passkey, and returns attestation artifacts to your app.

    Your app is responsible for starting recovery with your backend before calling the SDK and for finalizing recovery with your backend after the SDK returns.

    RecoveryInput contains three fields from your backend's recovery-start response: recoveryAttemptId is the server-issued correlation identifier your backend uses to track and finalize the attempt; recoveryToken is a short-lived, user-scoped authorization token the SDK presents to your backend's identity-verification proxy during the flow — treat it as a secret and do not log or display it; attemptExpiresAt is the expiry timestamp for the attempt.

    When the SDK invokes RecoveryRegistrationProvider, your backend should exchange the recoveryToken for a Core API continuation_token, obtain a replacement passkey registration challenge from your IDP, call the Core API recovery registration start endpoint with that continuation_token, and return the resulting replacement registration data to the SDK. Preserve the server-side context needed to prepare and finalize recovery after the SDK returns the replacement attestation.

  • android logoAndroid
  • ios logoiOS
  • kotlin
    1import com.entrust.identity.biometricpasskey.sdk.api.RecoveryRegistrationProvider
    2import com.entrust.identity.biometricpasskey.sdk.api.model.RecoveryInput
    3
    4suspend fun recoverAccount(start: RecoveryStartBundle) {
    5 val output = biometricPasskey.recover(
    6 input = RecoveryInput(
    7 recoveryAttemptId = start.recoveryAttemptId,
    8 recoveryToken = start.recoveryToken,
    9 attemptExpiresAt = start.attemptExpiresAt,
    10 ),
    11 registrationProvider = RecoveryRegistrationProvider { recoveryAttemptId, recoveryToken ->
    12 backend.startRecoveryRegistration(
    13 recoveryAttemptId = recoveryAttemptId,
    14 recoveryToken = recoveryToken,
    15 )
    16 },
    17 )
    18
    19 backend.completeRecoveryRegistration(
    20 recoveryAttemptId = start.recoveryAttemptId,
    21 credentialId = output.credentialId,
    22 attestationObject = output.attestationObject,
    23 clientDataJson = output.clientDataJson,
    24 )
    25}

    The registration provider should return RecoveryRegistrationData with the replacement WebAuthn challenge, non-empty user handle, relying-party ID, passkey registration expiry, recovery attempt expiry, and optional user ID returned by your backend. If your backend returns continuation data with the replacement registration bundle, such as a continuation token or owner context, preserve it in your app and send it when finalizing recovery.

    RecoveryRegistrationProvider is a fun interface (SAM interface), so you can pass a lambda directly instead of implementing a named class. If you prefer a named class for testability or reuse, implement the interface explicitly:

    kotlin
    1class AppRecoveryRegistrationProvider(
    2 private val backend: BackendClient,
    3) : RecoveryRegistrationProvider {
    4 override suspend fun fetchRegistrationData(
    5 recoveryAttemptId: String,
    6 recoveryToken: String,
    7 ): RecoveryRegistrationData = backend.startRecoveryRegistration(
    8 recoveryAttemptId = recoveryAttemptId,
    9 recoveryToken = recoveryToken,
    10 )
    11}
    swift
    1import BiometricPasskeySDK
    2
    3struct AppRecoveryRegistrationProvider: RecoveryRegistrationProvider {
    4 let backend: BackendClient
    5
    6 func fetchRegistrationData(
    7 recoveryAttemptId: String,
    8 recoveryToken: String
    9 ) async throws -> RecoveryRegistrationData {
    10 try await backend.startRecoveryRegistration(
    11 recoveryAttemptId: recoveryAttemptId,
    12 recoveryToken: recoveryToken
    13 )
    14 }
    15}
    16
    17func recoverAccount(_ start: RecoveryStartBundle) async throws {
    18 let output = try await biometricPasskey.recover(
    19 RecoveryInput(
    20 recoveryAttemptId: start.recoveryAttemptId,
    21 recoveryToken: start.recoveryToken,
    22 attemptExpiresAt: start.attemptExpiresAt
    23 ),
    24 registrationProvider: AppRecoveryRegistrationProvider(backend: backend)
    25 )
    26
    27 try await backend.completeRecoveryRegistration(
    28 recoveryAttemptId: start.recoveryAttemptId,
    29 credentialId: output.credentialId,
    30 attestationObject: output.attestationObject,
    31 clientDataJson: output.clientDataJson
    32 )
    33}

    The registration provider should return RecoveryRegistrationData with the replacement WebAuthn challenge, user handle, relying-party ID, passkey registration expiry, recovery attempt expiry, and any user ID returned by your backend. The iOS initializer can derive the user ID from userHandle if your backend omits it. If your backend returns continuation data with the replacement registration bundle, such as a continuation token or owner context, preserve it in your app and send it when finalizing recovery.

    Manage local credentials

    Credential management methods operate on local SDK storage only. They do not revoke credentials server-side. If your user deletes a credential from the device, your app should also call your backend to apply the corresponding credential lifecycle policy through your backend and the Management API.

  • android logoAndroid
  • ios logoiOS
  • kotlin
    1import com.entrust.identity.biometricpasskey.sdk.api.model.DeleteCredentialInput
    2import com.entrust.identity.biometricpasskey.sdk.api.model.GetCredentialInput
    3import com.entrust.identity.biometricpasskey.sdk.api.model.ListCredentialsInput
    4
    5val credentials = biometricPasskey.listCredentials(
    6 ListCredentialsInput(userId = currentUserId),
    7)
    8
    9val credential = biometricPasskey.getCredential(
    10 GetCredentialInput(credentialId = credentials.first().credentialId),
    11)
    12
    13biometricPasskey.deleteCredential(
    14 DeleteCredentialInput(credentialId = credential.credentialId),
    15)

    Each Credential exposes credentialId, rpDomain, createdAt, and lastUsedAt.

    swift
    1let credentials = try await biometricPasskey.listCredentials(
    2 ListCredentialsInput(userId: currentUserId)
    3)
    4
    5guard let first = credentials.first else { return }
    6
    7let credential = try await biometricPasskey.getCredential(
    8 GetCredentialInput(credentialId: first.credentialId)
    9)
    10
    11_ = try await biometricPasskey.deleteCredential(
    12 DeleteCredentialInput(credentialId: credential.credentialId)
    13)

    Each Credential exposes credentialId, rpDomain, createdAt, and lastUsedAt.

    Enable OS credential provider integration

    OS credential provider integration is optional. It lets platform passkey UI present SDK-managed credentials outside your in-app step-up flow. Enrollment still happens in the host app through enroll or recovery.

  • android logoAndroid
  • ios logoiOS
  • The Android SDK includes a Credential Provider service and activity in its manifest. These entries are merged into your app automatically. Use CredentialProviderStatus to check whether the user has enabled the provider and to open the relevant settings screen.

    kotlin
    1import com.entrust.identity.biometricpasskey.sdk.provider.CredentialProviderStatus
    2
    3val enabled: Boolean? = CredentialProviderStatus.isEnabled(context)
    4
    5if (enabled == false) {
    6 CredentialProviderStatus.openSettings(context)
    7}

    null means the provider status is unavailable, such as on unsupported Android versions.

    If you use the Android Credential Provider for native app callers, publish Digital Asset Links for your relying-party domain so Android can associate the caller with the RP. The SDK verifies https://<rpDomain>/.well-known/assetlinks.json for native callers unless Android supplies a trusted web origin. Include the delegate_permission/common.handle_all_urls relation, your app package name, and the signing certificate SHA-256 fingerprint.

    If you use a custom preferencesNamespace and need provider support before the host app initializes the SDK, set manifest metadata key com.entrust.identity.biometricpasskey.sdk.PREFERENCES_NAMESPACE to the same namespace.

    The Android provider uses the @drawable/biometric_passkey_provider_icon resource for passkey-provider UI surfaces. To use your own icon, add a drawable in your host app with the same resource name (biometric_passkey_provider_icon). Your app resource will override the SDK default.

    Example: add app/src/main/res/drawable/biometric_passkey_provider_icon.xml (or a PNG with the same resource name).

    To support iOS AutoFill, add a Credential Provider Extension target, sign it with the same team as the host app, and share the same Keychain access group. The extension view controller subclasses BiometricPasskeyCredentialProviderViewController.

    The icon shown in the iOS passkey selection sheet is your extension target's app icon. Set it by adding an AppIcon asset to the extension target's Assets.xcassets in Xcode — the OS picks it up automatically.

    swift
    1import BiometricPasskeySDK
    2
    3@available(iOS 17.0, *)
    4final class ExampleCredentialProviderViewController: BiometricPasskeyCredentialProviderViewController {
    5 override var keychainAccessGroup: String {
    6 "TEAMID.com.example.passkeys"
    7 }
    8
    9 override var rpDomain: String {
    10 "auth.example.com"
    11 }
    12}

    Sync local credentials to the system credential identity store after enrollment and when the app returns to the foreground. Remove identities on sign-out:

    swift
    1// After enrollment or when the app returns to the foreground:
    2await CredentialIdentityManager.syncCredentials(
    3 rpDomain: configuration.rpDomain,
    4 userId: currentUserId,
    5 accessGroup: configuration.keychainAccessGroup
    6)
    7
    8// Or sync all credentials for the RP domain (all users):
    9await CredentialIdentityManager.syncAllCredentials(
    10 rpDomain: configuration.rpDomain,
    11 accessGroup: configuration.keychainAccessGroup
    12)
    13
    14// On user sign-out — remove all registered identities from the system store:
    15await CredentialIdentityManager.removeAllIdentities()

    Use CredentialProviderStatus.isEnabled() and CredentialProviderStatus.openSettings() to guide the user to enable the extension:

    swift
    1let enabled = await CredentialProviderStatus.isEnabled()
    2
    3// isEnabled() returns Bool?. nil means state is unknown (e.g. on unsupported OS versions).
    4// Only prompt when the provider is confirmed disabled.
    5if enabled == false {
    6 await CredentialProviderStatus.openSettings()
    7}

    nil means the extension status could not be determined.

    The Credential Provider Extension supports assertion (authentication) only. Enrollment always happens in the host app through enroll or recover. The extension performs biometric evaluation via LAContext and signs with the Secure Enclave key independently of the host app process.

    iOS Credential Provider assertions may carry the WebAuthn backup eligibility and backup state flags because AuthenticationServices requires them for extension assertions. The SDK credential remains device-bound and non-exportable.

    Handle errors

    SDK operations fail with stable error codes. Branch on the code, not on the localized message. The categories below are implementation guidance, not an exhaustive enum list; tolerate unknown codes and fall back to isRetryable.

  • android logoAndroid
  • ios logoiOS
  • Android throws BiometricPasskeyException. Inspect code and isRetryable.

    kotlin
    1import com.entrust.identity.biometricpasskey.sdk.error.BiometricPasskeyErrorCode
    2import com.entrust.identity.biometricpasskey.sdk.error.BiometricPasskeyException
    3
    4try {
    5 approveHighRiskAction(start)
    6} catch (error: BiometricPasskeyException) {
    7 when (error.code) {
    8 BiometricPasskeyErrorCode.USER_CANCELLED -> showRetryOption()
    9 BiometricPasskeyErrorCode.CHALLENGE_EXPIRED -> restartFromBackend()
    10 BiometricPasskeyErrorCode.PASSKEY_NOT_FOUND -> offerRecovery()
    11 BiometricPasskeyErrorCode.RECOVERY_RATE_LIMITED -> showRateLimitMessage()
    12 BiometricPasskeyErrorCode.BIOMETRIC_LOCKOUT -> showBiometricLockedMessage()
    13 else -> if (error.isRetryable) showRetryOption() else showGenericFailure(error)
    14 }
    15}

    The BiometricPasskeyException class exposes:

    PropertyTypeDescription
    codeBiometricPasskeyErrorCodeStable enum value for programmatic branching.
    messageStringHuman-readable description (do not parse — use code for logic).
    isRetryableBooleanWhether the operation can be retried without obtaining new server state.
    causeThrowable?Underlying exception when available.

    iOS throws BiometricPasskeyError. Inspect code and isRetryable.

    swift
    1do {
    2 try await approveHighRiskAction(start)
    3} catch let error as BiometricPasskeyError {
    4 switch error.code {
    5 case .userCancelled:
    6 showRetryOption()
    7 case .challengeExpired:
    8 restartFromBackend()
    9 case .passkeyNotFound:
    10 offerRecovery()
    11 case .recoveryRateLimited:
    12 showRateLimitMessage()
    13 case .hardwareNotAvailable:
    14 showDeviceNotSupportedMessage()
    15 case .biometricLockout:
    16 showBiometricLockedMessage()
    17 default:
    18 if error.isRetryable {
    19 showRetryOption()
    20 } else {
    21 showGenericFailure(error)
    22 }
    23 }
    24}

    The BiometricPasskeyError struct exposes:

    PropertyTypeDescription
    codeBiometricPasskeyErrorCodeStable enum case for programmatic branching.
    messageStringHuman-readable description (do not parse — use code for logic).
    isRetryableBoolWhether the operation can be retried without obtaining new server state.
    cause(any Error & Sendable)?Underlying system error when available.

    The error code table below uses the canonical UPPER_SNAKE_CASE identifiers. On Android these map directly to BiometricPasskeyErrorCode enum constants (e.g. BiometricPasskeyErrorCode.PASSKEY_NOT_FOUND). On iOS, Swift represents the same codes as camelCase enum cases (e.g. .passkeyNotFound), but each case's rawValue is the identical UPPER_SNAKE_CASE string. Use the canonical name when logging or sending error codes to your backend so reports are consistent across platforms.

    CategoryTypical codesRecommended response
    ConfigurationINVALID_CONFIGURATIONDev-time misconfiguration: incorrect rpDomain, serverBaseUrl, keychain access group, or missing permissions. Fix before shipping; should not appear in production.
    Invalid inputINVALID_INPUTA required field in the SDK input bundle was missing or malformed. Usually caused by a backend response mapping error. Monitor in production — repeated occurrences indicate a backend contract change.
    Network or backend failureNETWORK_ERROR, SERVER_ERROR, TIMEOUTRetry with backoff when the user journey can safely resume. Preserve backend correlation IDs in your logs.
    Challenge or session lifecycleCHALLENGE_EXPIRED, CHALLENGE_CONSUMED, CHALLENGE_REFRESH_FORBIDDEN, INVALID_OR_EXPIRED_SESSION, AUTH_CONTEXT_MISMATCHRestart the flow from your backend and obtain a fresh SDK input bundle.
    User cancelationUSER_CANCELLEDKeep this as a soft cancel and let the user retry from your app.
    Local credential mismatchPASSKEY_NOT_FOUND, CREDENTIAL_DELETE_FORBIDDENOffer recovery or another customer-approved fallback. For delete-forbidden, surface a non-destructive message and rely on backend lifecycle policy.
    RecoveryRECOVERY_NOT_SUPPORTED, RECOVERY_ATTEMPT_EXPIRED, RECOVERY_ATTEMPT_CANCELLED, RECOVERY_PASSKEY_REGISTRATION_EXPIRED, RECOVERY_TOKEN_INVALID, RECOVERY_EBT_UNAVAILABLE, RECOVERY_MATCH_FAILED, RECOVERY_RATE_LIMITED, RECOVERY_IDV_NOT_COMPLETED, RECOVERY_REGISTRATION_FAILED, RECOVERY_REGISTRATION_MISMATCHFollow your recovery policy. Do not retry token, attempt, or expiry errors without obtaining fresh backend state.
    Biometric (Android and iOS)BIOMETRIC_LOCKOUTBiometric authenticator is temporarily locked. Prompt the user to unlock their device and try again. Retryable.
    Hardware (iOS only)HARDWARE_NOT_AVAILABLEDevice does not support Secure Enclave or key generation failed. Guide the user to a supported device.
    Registration lifecycleREGISTRATION_ATTEMPT_CONFLICT, REGISTRATION_ATTEMPT_EXPIRED, REGISTRATION_ATTEMPT_CANCELLED, REGISTRATION_ATTEMPT_CLEANED_UPRestart registration from your backend.
    Finalize lifecycleIDEMPOTENT_REPLAY, IDP_COMMIT_FAILED, FINALIZE_TOKEN_INVALID, RESERVATION_EXPIREDTreat as backend finalization failures surfaced through your backend response after the SDK returns. Restart the flow when token or reservation state is no longer valid.
    Workflow / IDVWORKFLOW_NOT_COMPLETED, WORKFLOW_DECLINED, WORKFLOW_UNDER_REVIEW, WORKFLOW_RUN_REUSE_FORBIDDENIDV was not completed, was declined by the IDV provider, or is pending manual review. For WORKFLOW_DECLINED and WORKFLOW_UNDER_REVIEW, surface appropriate messaging to the user and do not retry automatically. For WORKFLOW_NOT_COMPLETED and WORKFLOW_RUN_REUSE_FORBIDDEN, obtain a fresh workflow run from your backend.
    Step-upSTEP_UP_CREDENTIAL_USER_MISMATCH, STEP_UP_AUTH_SESSION_MISMATCHCredential does not belong to the session user, or the session does not match the credential's auth context. Verify your backend is sending the correct allow-list and session.

    Security, privacy, and local storage

  • android logoAndroid
  • ios logoiOS
  • AreaDetails
    Key custodyPasskey keys are generated in Android Keystore for ES256 signing. StrongBox is used when available, with TEE fallback.
    Credential metadataStored in SDK-owned encrypted storage under the configured preferencesNamespace.
    SigningStandard local signing and provider signing require user presence through platform biometric UI. Step-up flows use Entrust Identity Verification as the identity gate before assertion output is returned.
    TransportSDK network calls use serverBaseUrl and require HTTPS for remote hosts.
    Biometric dataThe SDK does not expose raw biometric data or raw encrypted biometric tokens to your app.
    AreaDetails
    Key custodyPasskey keys require Secure Enclave-backed P-256 signing and remain non-exportable. The SDK fails with .hardwareNotAvailable when Secure Enclave key generation is unavailable.
    Credential metadataStored in Keychain under the configured access group. Use a shared access group for a Credential Provider Extension.
    SigningStandard local signing and extension signing require user presence through LocalAuthentication. Step-up flows use Entrust Identity Verification as the identity gate before assertion output is returned.
    TransportSDK network calls use serverBaseUrl and require HTTPS for remote hosts. App Transport Security remains in effect.
    Biometric dataThe SDK does not expose raw biometric data or raw encrypted biometric tokens to your app.

    Passkey keys are bound to the device's secure hardware and cannot be exported or migrated. If a user wipes, replaces, or loses their device, all SDK-enrolled credentials on that device are permanently lost. Your app must offer account recovery so users can re-enroll on a new device. Design your support and customer-service flows accordingly.

    Testing and release readiness

    Unit testing with mocks

    Both SDKs expose a mockable type for dependency injection. Declare the injectable type in your production code and supply a test double in unit tests.

  • android logoAndroid
  • ios logoiOS
  • BiometricPasskeyClient is an interface. Inject it into your classes and implement a test double:

    kotlin
    1// Production code — depend on the interface
    2class PasskeyViewModel(private val sdk: BiometricPasskeyClient) : ViewModel() { ... }
    3
    4// Test code — implement a minimal fake
    5class FakeBiometricPasskeyClient : BiometricPasskeyClient {
    6 override suspend fun enroll(input: EnrollmentInput): EnrollmentOutput =
    7 EnrollmentOutput(
    8 credentialId = "test-credential-id",
    9 attestationObject = "test-attestation",
    10 clientDataJson = "test-client-data",
    11 )
    12 override suspend fun authenticate(input: AuthenticationInput): AuthenticationOutput = TODO()
    13 override suspend fun recover(input: RecoveryInput, registrationProvider: RecoveryRegistrationProvider): RecoveredCredentialOutput = TODO()
    14 override suspend fun listCredentials(input: ListCredentialsInput): List<Credential> = emptyList()
    15 override suspend fun getCredential(input: GetCredentialInput): Credential = TODO()
    16 override suspend fun deleteCredential(input: DeleteCredentialInput): Credential = TODO()
    17}

    BiometricPasskeyClientProtocol is the injectable type. Declare it in your production code and implement a test double conforming to the protocol:

    swift
    1// Production code — depend on the protocol
    2final class PasskeyViewModel {
    3 private let sdk: BiometricPasskeyClientProtocol
    4 init(sdk: BiometricPasskeyClientProtocol) { self.sdk = sdk }
    5}
    6
    7// Test code — implement a minimal fake
    8final class FakeBiometricPasskeyClient: BiometricPasskeyClientProtocol {
    9 var enrollResult = EnrollmentOutput(
    10 credentialId: "test-credential-id",
    11 attestationObject: "test-attestation",
    12 clientDataJson: "test-client-data"
    13 )
    14
    15 func enroll(_ input: EnrollmentInput) async throws -> EnrollmentOutput { enrollResult }
    16 func authenticate(_ input: AuthenticationInput) async throws -> AuthenticationOutput { fatalError("not implemented") }
    17 func recover(_ input: RecoveryInput, registrationProvider: any RecoveryRegistrationProvider) async throws -> RecoveredCredentialOutput { fatalError("not implemented") }
    18 func listCredentials(_ input: ListCredentialsInput) async throws -> [Credential] { [] }
    19 func getCredential(_ input: GetCredentialInput) async throws -> Credential { fatalError("not implemented") }
    20 func deleteCredential(_ input: DeleteCredentialInput) async throws -> Credential { fatalError("not implemented") }
    21}

    Integration checklist

    Before shipping, validate each integration boundary end to end:

    AreaWhat to verify
    InstallationAndroid resolves com.entrust.identity.biometricpasskey:android-sdk:<version> from Maven Central. iOS resolves BiometricPasskeySDK from the public GitHub Swift package repository and its hosted XCFramework release asset.
    Version policySDK versions are supported for your backend release line according to the compatibility matrix in Biometric Passkey: Version policy.
    ConfigurationrpDomain, serverBaseUrl, Android storage namespace, and iOS keychain access group match your deployed backend and app entitlements. The SDK-owned mobile proxy paths listed in Backend contract for SDK calls are exposed by your backend at the exact paths shown.
    EnrollmentThe app receives a registration bundle, the SDK returns attestation artifacts, and the backend finalizes registration.
    In-app step-upThe app receives an authentication bundle, the SDK starts identity verification through your mobile proxy, and the backend completes the assertion.
    Cross-platform step-upHandoff token delivery, session resume, mobile authentication, and browser-side status handling complete for each delivery channel you support.
    Recoveryrecover runs identity verification, the registration provider returns a replacement registration bundle, and the backend finalizes the replacement credential.
    Local credentialsLocal list/get/delete behavior matches your UX, and server-side credential lifecycle actions happen through your backend policy.
    Provider integrationAndroid provider and iOS AutoFill are tested separately from in-app step-up.
    ErrorsYour app handles user cancelation, expired challenges, missing credentials, recovery failures, and retryable transport failures.

    SDK API reference

  • android logoAndroid
  • ios logoiOS
  • MethodPurpose
    BiometricPasskeyClient.create(context, configuration)Creates the SDK instance.
    enroll(EnrollmentInput)Runs enrollment identity verification and returns attestation artifacts.
    authenticate(AuthenticationInput)Signs an assertion for standard authentication, in-app step-up, or cross-platform step-up.
    recover(RecoveryInput, RecoveryRegistrationProvider)Runs recovery identity verification, obtains a replacement registration bundle through your callback, and returns attestation artifacts.
    listCredentials(ListCredentialsInput)Lists local credential metadata for a user. Each Credential includes credentialId, rpDomain, createdAt, and lastUsedAt.
    getCredential(GetCredentialInput)Reads one local credential metadata record.
    deleteCredential(DeleteCredentialInput)Deletes one local credential and its local key material.
    MethodPurpose
    BiometricPasskeyClient(configuration:)Creates the SDK instance from a validated BiometricPasskeyConfiguration.
    enroll(_: EnrollmentInput) -> EnrollmentOutputRuns enrollment identity verification and returns credentialId, attestationObject, and clientDataJson as base64url-encoded strings.
    authenticate(_: AuthenticationInput) -> AuthenticationOutputSigns an assertion for standard authentication, in-app step-up, or cross-platform step-up. Returns credentialId, authenticatorData, clientDataJson, signature, biometricPasskeySessionId, and userHandle. All binary fields are base64url-encoded strings.
    recover(_: RecoveryInput, registrationProvider:) -> RecoveredCredentialOutputRuns recovery identity verification, obtains a replacement registration bundle through your RecoveryRegistrationProvider, and returns attestation artifacts.
    listCredentials(_: ListCredentialsInput) -> [Credential]Lists local credential metadata for a user. Each Credential includes credentialId, rpDomain, createdAt, and lastUsedAt.
    getCredential(_: GetCredentialInput) -> CredentialReads one local credential metadata record.
    deleteCredential(_: DeleteCredentialInput) -> CredentialDeletes one local credential and its Secure Enclave key material. Returns the deleted credential metadata.

    All methods are async throws and throw BiometricPasskeyError on failure. The SDK conforms to BiometricPasskeyClientProtocol for dependency injection and test mocking.