Skip to content

feat(expo-biometrics): add biometric credential native module - #9959

Draft
mikepitre wants to merge 3 commits into
mike/clerk-js-trusted-device-resourcesfrom
mike/expo-biometrics-ios
Draft

mikepitre wants to merge 3 commits into
mike/clerk-js-trusted-device-resourcesfrom
mike/expo-biometrics-ios

Conversation

@mikepitre

@mikepitre mikepitre commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Description

Adds @clerk/expo-biometrics, an experimental Expo native module that handles only the device side of Clerk biometric credentials (trusted_device): Secure Enclave key creation, ES256 signing behind a LocalAuthentication prompt, and the on-device credential records. It does not depend on clerk-ios or clerk-android and makes no FAPI calls. Enrollment, listing, revocation and sign-in move into the @clerk/expo hooks on top of the clerk-js resources from #9953 in #9960. Reverification stays on the native SDK in @clerk/expo-native-components for now, because FAPI only lists trusted-device reverification factors for API version 2026-08-20 and later, and clerk-js doesn't send that version yet.

Why a separate package instead of @clerk/expo-native-components or @clerk/expo

  • Not @clerk/expo-native-components. That package exists to hold the Clerk native SDKs (clerk-ios / clerk-android): the full SDKs, an iOS 17 minimum, the Swift Package and Gradle setup, and a second Clerk client running at runtime. This module needs none of that. Putting it there would make a JS-only app that wants Face ID take on all of it, which is the cost feat(expo-native-components): move native components into @clerk/expo-native-components #9955 removes.
  • Not @clerk/expo. Expo autolinks every native module in an installed package, so every @clerk/expo app would link LocalAuthentication, androidx.biometric and this module whether it uses biometrics or not. Apps that ship Face ID code also have to declare NSFaceIDUsageDescription. Only apps that use biometrics should carry that.
  • Same pattern as @clerk/expo-passkeys and @clerk/expo-google-signin: a thin, optional platform package that plugs into @clerk/expo.

Trade-offs:

The iOS implementation follows the clerk-ios biometric credential storage contract v1 (clerk/clerk-ios#584) so that credentials created here and by ClerkKit in the same app are interchangeable:

  • Secure Enclave P-256 keys tagged dev.clerk.trusted_device.<localKeyId> with the contract's access-control flags per policy; JWK and raw r || s signature encoding match the contract byte for byte.
  • Records live in the shared trustedDeviceCredentials generic-password item (service ClerkKeychainService ?? bundleId, no access group). Reads skip malformed records and writes keep unknown fields and other apps' records.
  • The reinstall marker in UserDefaults uses the same key as ClerkKit. Every store operation applies it first, so ClerkKit's first configuration does not wipe records this module wrote.
  • Store read-modify-writes run on the main queue, which serializes them with ClerkKit's @MainActor store access in the same process.

JS API: getAppIdentifier, getAvailability, createKey, sign, hasKey, deleteKey, listRecords, saveRecord, deleteRecord, ensureInstallationMarker. Every error is a ClerkBiometricsError with a stable code.

Android is a stub in this PR: every call rejects with not_implemented. The Android implementation is in #9961, following the clerk-android v2 storage format (clerk/clerk-android#966).

The module's Swift unit tests (a test_spec in the podspec) pin the contract with the same vectors and v1 fixture as clerk-ios. The Expo native build workflow now packs this package into its fixture, so CI compiles it on iOS and Android.

Depends on the storage contract in clerk/clerk-ios#584.

Checklist

  • pnpm test runs as expected.
  • pnpm build runs as expected.
  • (If applicable) JSDoc comments have been added or updated for any package exports
  • (If applicable) Documentation has been updated

Type of change

  • 🐛 Bug fix
  • 🌟 New feature
  • 🔨 Breaking change
  • 📖 Refactoring / dependency upgrade / documentation
  • other:

🤖 Generated with Claude Code

@vercel

vercel Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
clerk-js-sandbox Ready Ready Preview Sep 28, 2026 8:26pm UTC
swingset Ready Ready Preview Sep 28, 2026 8:26pm UTC

Request Review

@changeset-bot

changeset-bot Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ca1e44b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@clerk/expo-biometrics Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Comment @coderabbitai help to get the list of available commands.

@mikepitre
mikepitre force-pushed the mike/expo-biometrics-ios branch from 911b510 to 0505abb Compare September 27, 2026 15:17
@mikepitre
mikepitre changed the base branch from main to mike/clerk-js-trusted-device-resources September 27, 2026 15:17
@vercel
vercel Bot temporarily deployed to Preview – clerk-js-sandbox September 28, 2026 15:03 Inactive
@vercel
vercel Bot temporarily deployed to Preview – clerk-js-sandbox September 28, 2026 19:06 Inactive
@mikepitre
mikepitre force-pushed the mike/clerk-js-trusted-device-resources branch from 03f95ab to aa7405a Compare September 28, 2026 19:35
@mikepitre
mikepitre force-pushed the mike/expo-biometrics-ios branch from 19adb60 to 4455c35 Compare September 28, 2026 19:35
@mikepitre

mikepitre commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor Author

Superseded by #9959 (comment) (re-recorded with full-frame-rate capture).

mikepitre and others added 3 commits September 28, 2026 16:10
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ntract tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add secureKeyStorageAvailable to getAvailability() and reject createKey()
with secure_key_storage_unavailable when the device has no Secure Enclave,
such as the iOS Simulator, instead of failing inside SecKeyCreateRandomKey.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikepitre

Copy link
Copy Markdown
Contributor Author

Re-recorded with full-frame-rate simulator capture (simctl io recordVideo). iPhone Air simulator (iOS 27): the JS-only Expo SDK 57 test app with @clerk/expo + @clerk/expo-biometrics from the top of the stack and without @clerk/expo-native-components.

  • Build: ClerkExpoBiometrics is the only Clerk pod (no ClerkExpo / ClerkKit), and the binary has no ClerkKit symbols. On screen: "Native Clerk module: not linked", "@clerk/expo-biometrics module: linked".
  • The simulator has no usable Secure Enclave, so getAvailability() reports biometric_authentication_unavailable. Signed out, signIn() fails fast with that reason; signed in (email code), enroll() fails with biometric_authentication_unavailable before any Face ID prompt or FAPI call.

The successful Face ID enroll + sign-in on a physical iPhone is recorded on #9960.

jsonly-v2-final.mp4

This branch was successfully deployed

2 active deployments
Preview – swingset — ca1e44bb Deployed Sep 28, 2026 by vercel[bot]
Preview – clerk-js-sandbox — ca1e44bb Deployed Sep 28, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant