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
| Requirement | Notes |
|---|---|
| Android version | Android 9 (API level 28) or later. |
| Compile SDK | API level 37. |
| Language and runtime | Kotlin with Java/JVM target 17. SDK operations are suspend functions. |
| Host activity | Use a FragmentActivity or AppCompatActivity host for flows that show Entrust Identity Verification or biometric UI. |
| Networking | serverBaseUrl must use HTTPS for remote hosts. HTTP is accepted only for localhost or 127.0.0.1 during local development. |
| Optional provider integration | Android 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.
| Requirement | Notes |
|---|---|
| iOS version | iOS 17.0 or later. |
| Language and runtime | Swift with Swift Concurrency (async / await). |
| Frameworks | LocalAuthentication, AuthenticationServices, Keychain, and Secure Enclave-backed passkey operations. |
| Networking | serverBaseUrl must use HTTPS for remote hosts. HTTP is accepted only for localhost or 127.0.0.1 during local development. |
| Keychain access group | Required to scope SDK credential metadata and passkey references. Use a shared access group if you add a Credential Provider Extension. |
| Optional provider integration | iOS 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
The Android SDK is published to Maven Central. Use the version shipped with your release:
1// app/build.gradle.kts2dependencies {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:
1// settings.gradle.kts2dependencyResolutionManagement {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.
1// Package.swift2dependencies: [3 .package(url: "https://github.com/EntrustCorporation/biometric-passkey-ios", from: "<version>")4]56// 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:
- Add
NSCameraUsageDescriptionto your app'sInfo.plist(required for identity verification capture). - Add
NSFaceIDUsageDescriptionto your app'sInfo.plist(required for the LocalAuthentication prompts used by signing and provider flows). - Enable the Keychain Sharing capability and add your Keychain access group.
- If you use a Credential Provider Extension, add the AutoFill Credential Provider capability to the extension target.
- 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
1// 1. Build configuration2val configuration = BiometricPasskeyConfiguration(3 rpDomain = "auth.example.com",4 serverBaseUrl = "https://api.example.com",5)67// 2. Create the SDK instance in Activity.onCreate8val biometricPasskey: BiometricPasskeyClient = BiometricPasskeyClient.create(this, configuration)910// 3. Enroll11val 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)
1// 1. Build configuration2let configuration = try BiometricPasskeyConfiguration(3 rpDomain: "auth.example.com",4 serverBaseUrl: URL(string: "https://api.example.com")!,5 keychainAccessGroup: "TEAMID.com.example.passkeys"6)78// 2. Create the SDK instance9let biometricPasskey: BiometricPasskeyClientProtocol = BiometricPasskeyClient(configuration: configuration)1011// 3. Enroll12let 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.expiresAt20 )21)22try await backend.completeRegistration(23 registrationAttemptId: start.registrationAttemptId,24 credentialId: output.credentialId,25 attestationObject: output.attestationObject,26 clientDataJson: output.clientDataJson27)
Authenticate
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)
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.biometricPasskeySessionId7 )8)9try await backend.completeAuthentication(10 biometricPasskeySessionId: output.biometricPasskeySessionId,11 credentialId: output.credentialId,12 authenticatorData: output.authenticatorData,13 clientDataJson: output.clientDataJson,14 signature: output.signature15)
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.
1import com.entrust.identity.biometricpasskey.sdk.api.BiometricPasskeyConfiguration2import com.entrust.identity.biometricpasskey.sdk.api.BiometricPasskeyStorageConfiguration34val configuration = BiometricPasskeyConfiguration(5 rpDomain = "auth.example.com",6 serverBaseUrl = "https://api.example.com",7 storageConfiguration = BiometricPasskeyStorageConfiguration(8 preferencesNamespace = "example_prod",9 ),10)
| Setting | Details |
|---|---|
rpDomain | Fully qualified relying-party domain used for WebAuthn credential operations. It must match the relying-party ID your backend uses. |
serverBaseUrl | HTTPS base URL for your mobile backend surface. The SDK appends its mobile proxy paths to this URL. |
preferencesNamespace | Optional 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.
1import BiometricPasskeySDK23let configuration = try BiometricPasskeyConfiguration(4 rpDomain: "auth.example.com",5 serverBaseUrl: URL(string: "https://api.example.com")!,6 keychainAccessGroup: "TEAMID.com.example.passkeys"7)
| Setting | Details |
|---|---|
rpDomain | Fully qualified relying-party domain used for WebAuthn credential operations. It must match the relying-party ID your backend uses. |
serverBaseUrl | HTTPS base URL for your mobile backend surface. The SDK appends its mobile proxy paths to this URL. |
keychainAccessGroup | Keychain 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
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.
1import android.os.Bundle2import androidx.fragment.app.FragmentActivity3import com.entrust.identity.biometricpasskey.sdk.api.BiometricPasskeyClient45class MainActivity : FragmentActivity() {6 private lateinit var biometricPasskey: BiometricPasskeyClient78 override fun onCreate(savedInstanceState: Bundle?) {9 super.onCreate(savedInstanceState)1011 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.
1import BiometricPasskeySDK2import SwiftUI34@main5struct ExampleApp: App {6 let biometricPasskey: BiometricPasskeyClientProtocol78 init() {9 let configuration: BiometricPasskeyConfiguration10 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 }2122 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.
| Flow | App obtains before SDK call | SDK returns to app | Backend completes after SDK call |
|---|---|---|---|
| Enrollment | Registration 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-up | Authentication 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-up | Resumed 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. |
| Recovery | Recovery 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:
| Purpose | Mobile 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.
1import com.entrust.identity.biometricpasskey.sdk.api.model.EnrollmentInput23suspend 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 )1415 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.
1import BiometricPasskeySDK23func 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.userHandle12 )13 )1415 try await backend.completeRegistration(16 registrationAttemptId: start.registrationAttemptId,17 credentialId: output.credentialId,18 attestationObject: output.attestationObject,19 clientDataJson: output.clientDataJson20 )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.
1import com.entrust.identity.biometricpasskey.sdk.api.model.AuthenticationInput23suspend 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 )1213 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.
1import BiometricPasskeySDK23func 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.crossPlatformSessionId10 )11 )1213 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.userHandle20 )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:
- Receive the handoff token through your chosen channel.
- Send the token to your backend to resume the cross-platform session.
- Ask your backend for the mobile authentication bundle for that resumed session.
- Call
authenticatewith bothbiometricPasskeySessionIdandcrossPlatformSessionId. - 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.
1suspend fun handleCrossPlatformHandoff(handoffToken: String) {2 val resumed = backend.resumeCrossPlatformSession(handoffToken)3 val start = backend.startCrossPlatformAuthentication(4 crossPlatformSessionId = resumed.crossPlatformSessionId,5 )67 val output = biometricPasskey.authenticate(8 AuthenticationInput(9 challenge = start.challenge,10 allowCredentials = start.allowCredentials,11 biometricPasskeySessionId = start.biometricPasskeySessionId,12 crossPlatformSessionId = resumed.crossPlatformSessionId,13 ),14 )1516 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}
1func handleCrossPlatformHandoff(_ handoffToken: String) async throws {2 let resumed = try await backend.resumeCrossPlatformSession(handoffToken)3 let start = try await backend.startCrossPlatformAuthentication(4 crossPlatformSessionId: resumed.crossPlatformSessionId5 )67 let output = try await biometricPasskey.authenticate(8 AuthenticationInput(9 challenge: start.challenge,10 allowCredentials: start.allowCredentials,11 biometricPasskeySessionId: start.biometricPasskeySessionId,12 crossPlatformSessionId: resumed.crossPlatformSessionId13 )14 )1516 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.userHandle23 )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.
1import com.entrust.identity.biometricpasskey.sdk.api.RecoveryRegistrationProvider2import com.entrust.identity.biometricpasskey.sdk.api.model.RecoveryInput34suspend 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 )1819 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:
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}
1import BiometricPasskeySDK23struct AppRecoveryRegistrationProvider: RecoveryRegistrationProvider {4 let backend: BackendClient56 func fetchRegistrationData(7 recoveryAttemptId: String,8 recoveryToken: String9 ) async throws -> RecoveryRegistrationData {10 try await backend.startRecoveryRegistration(11 recoveryAttemptId: recoveryAttemptId,12 recoveryToken: recoveryToken13 )14 }15}1617func 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.attemptExpiresAt23 ),24 registrationProvider: AppRecoveryRegistrationProvider(backend: backend)25 )2627 try await backend.completeRecoveryRegistration(28 recoveryAttemptId: start.recoveryAttemptId,29 credentialId: output.credentialId,30 attestationObject: output.attestationObject,31 clientDataJson: output.clientDataJson32 )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.
1import com.entrust.identity.biometricpasskey.sdk.api.model.DeleteCredentialInput2import com.entrust.identity.biometricpasskey.sdk.api.model.GetCredentialInput3import com.entrust.identity.biometricpasskey.sdk.api.model.ListCredentialsInput45val credentials = biometricPasskey.listCredentials(6 ListCredentialsInput(userId = currentUserId),7)89val credential = biometricPasskey.getCredential(10 GetCredentialInput(credentialId = credentials.first().credentialId),11)1213biometricPasskey.deleteCredential(14 DeleteCredentialInput(credentialId = credential.credentialId),15)
Each Credential exposes credentialId, rpDomain, createdAt, and lastUsedAt.
1let credentials = try await biometricPasskey.listCredentials(2 ListCredentialsInput(userId: currentUserId)3)45guard let first = credentials.first else { return }67let credential = try await biometricPasskey.getCredential(8 GetCredentialInput(credentialId: first.credentialId)9)1011_ = 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.
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.
1import com.entrust.identity.biometricpasskey.sdk.provider.CredentialProviderStatus23val enabled: Boolean? = CredentialProviderStatus.isEnabled(context)45if (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.
1import BiometricPasskeySDK23@available(iOS 17.0, *)4final class ExampleCredentialProviderViewController: BiometricPasskeyCredentialProviderViewController {5 override var keychainAccessGroup: String {6 "TEAMID.com.example.passkeys"7 }89 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:
1// After enrollment or when the app returns to the foreground:2await CredentialIdentityManager.syncCredentials(3 rpDomain: configuration.rpDomain,4 userId: currentUserId,5 accessGroup: configuration.keychainAccessGroup6)78// Or sync all credentials for the RP domain (all users):9await CredentialIdentityManager.syncAllCredentials(10 rpDomain: configuration.rpDomain,11 accessGroup: configuration.keychainAccessGroup12)1314// 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:
1let enabled = await CredentialProviderStatus.isEnabled()23// 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 throws BiometricPasskeyException. Inspect code and isRetryable.
1import com.entrust.identity.biometricpasskey.sdk.error.BiometricPasskeyErrorCode2import com.entrust.identity.biometricpasskey.sdk.error.BiometricPasskeyException34try {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:
| Property | Type | Description |
|---|---|---|
code | BiometricPasskeyErrorCode | Stable enum value for programmatic branching. |
message | String | Human-readable description (do not parse — use code for logic). |
isRetryable | Boolean | Whether the operation can be retried without obtaining new server state. |
cause | Throwable? | Underlying exception when available. |
iOS throws BiometricPasskeyError. Inspect code and isRetryable.
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:
| Property | Type | Description |
|---|---|---|
code | BiometricPasskeyErrorCode | Stable enum case for programmatic branching. |
message | String | Human-readable description (do not parse — use code for logic). |
isRetryable | Bool | Whether 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.
| Category | Typical codes | Recommended response |
|---|---|---|
| Configuration | INVALID_CONFIGURATION | Dev-time misconfiguration: incorrect rpDomain, serverBaseUrl, keychain access group, or missing permissions. Fix before shipping; should not appear in production. |
| Invalid input | INVALID_INPUT | A 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 failure | NETWORK_ERROR, SERVER_ERROR, TIMEOUT | Retry with backoff when the user journey can safely resume. Preserve backend correlation IDs in your logs. |
| Challenge or session lifecycle | CHALLENGE_EXPIRED, CHALLENGE_CONSUMED, CHALLENGE_REFRESH_FORBIDDEN, INVALID_OR_EXPIRED_SESSION, AUTH_CONTEXT_MISMATCH | Restart the flow from your backend and obtain a fresh SDK input bundle. |
| User cancelation | USER_CANCELLED | Keep this as a soft cancel and let the user retry from your app. |
| Local credential mismatch | PASSKEY_NOT_FOUND, CREDENTIAL_DELETE_FORBIDDEN | Offer recovery or another customer-approved fallback. For delete-forbidden, surface a non-destructive message and rely on backend lifecycle policy. |
| Recovery | RECOVERY_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_MISMATCH | Follow your recovery policy. Do not retry token, attempt, or expiry errors without obtaining fresh backend state. |
| Biometric (Android and iOS) | BIOMETRIC_LOCKOUT | Biometric authenticator is temporarily locked. Prompt the user to unlock their device and try again. Retryable. |
| Hardware (iOS only) | HARDWARE_NOT_AVAILABLE | Device does not support Secure Enclave or key generation failed. Guide the user to a supported device. |
| Registration lifecycle | REGISTRATION_ATTEMPT_CONFLICT, REGISTRATION_ATTEMPT_EXPIRED, REGISTRATION_ATTEMPT_CANCELLED, REGISTRATION_ATTEMPT_CLEANED_UP | Restart registration from your backend. |
| Finalize lifecycle | IDEMPOTENT_REPLAY, IDP_COMMIT_FAILED, FINALIZE_TOKEN_INVALID, RESERVATION_EXPIRED | Treat 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 / IDV | WORKFLOW_NOT_COMPLETED, WORKFLOW_DECLINED, WORKFLOW_UNDER_REVIEW, WORKFLOW_RUN_REUSE_FORBIDDEN | IDV 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-up | STEP_UP_CREDENTIAL_USER_MISMATCH, STEP_UP_AUTH_SESSION_MISMATCH | Credential 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
| Area | Details |
|---|---|
| Key custody | Passkey keys are generated in Android Keystore for ES256 signing. StrongBox is used when available, with TEE fallback. |
| Credential metadata | Stored in SDK-owned encrypted storage under the configured preferencesNamespace. |
| Signing | Standard 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. |
| Transport | SDK network calls use serverBaseUrl and require HTTPS for remote hosts. |
| Biometric data | The SDK does not expose raw biometric data or raw encrypted biometric tokens to your app. |
| Area | Details |
|---|---|
| Key custody | Passkey 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 metadata | Stored in Keychain under the configured access group. Use a shared access group for a Credential Provider Extension. |
| Signing | Standard 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. |
| Transport | SDK network calls use serverBaseUrl and require HTTPS for remote hosts. App Transport Security remains in effect. |
| Biometric data | The 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.
BiometricPasskeyClient is an interface. Inject it into your classes and implement a test double:
1// Production code — depend on the interface2class PasskeyViewModel(private val sdk: BiometricPasskeyClient) : ViewModel() { ... }34// Test code — implement a minimal fake5class 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:
1// Production code — depend on the protocol2final class PasskeyViewModel {3 private let sdk: BiometricPasskeyClientProtocol4 init(sdk: BiometricPasskeyClientProtocol) { self.sdk = sdk }5}67// Test code — implement a minimal fake8final class FakeBiometricPasskeyClient: BiometricPasskeyClientProtocol {9 var enrollResult = EnrollmentOutput(10 credentialId: "test-credential-id",11 attestationObject: "test-attestation",12 clientDataJson: "test-client-data"13 )1415 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:
| Area | What to verify |
|---|---|
| Installation | Android 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 policy | SDK versions are supported for your backend release line according to the compatibility matrix in Biometric Passkey: Version policy. |
| Configuration | rpDomain, 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. |
| Enrollment | The app receives a registration bundle, the SDK returns attestation artifacts, and the backend finalizes registration. |
| In-app step-up | The app receives an authentication bundle, the SDK starts identity verification through your mobile proxy, and the backend completes the assertion. |
| Cross-platform step-up | Handoff token delivery, session resume, mobile authentication, and browser-side status handling complete for each delivery channel you support. |
| Recovery | recover runs identity verification, the registration provider returns a replacement registration bundle, and the backend finalizes the replacement credential. |
| Local credentials | Local list/get/delete behavior matches your UX, and server-side credential lifecycle actions happen through your backend policy. |
| Provider integration | Android provider and iOS AutoFill are tested separately from in-app step-up. |
| Errors | Your app handles user cancelation, expired challenges, missing credentials, recovery failures, and retryable transport failures. |
SDK API reference
| Method | Purpose |
|---|---|
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. |
| Method | Purpose |
|---|---|
BiometricPasskeyClient(configuration:) | Creates the SDK instance from a validated BiometricPasskeyConfiguration. |
enroll(_: EnrollmentInput) -> EnrollmentOutput | Runs enrollment identity verification and returns credentialId, attestationObject, and clientDataJson as base64url-encoded strings. |
authenticate(_: AuthenticationInput) -> AuthenticationOutput | Signs 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:) -> RecoveredCredentialOutput | Runs 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) -> Credential | Reads one local credential metadata record. |
deleteCredential(_: DeleteCredentialInput) -> Credential | Deletes 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.


