Skip to content

feat(expo): move biometric credentials to JS with @clerk/expo-biometrics - #9989

Draft
mikepitre wants to merge 11 commits into
mainfrom
mike/expo-biometrics-package
Draft

mikepitre wants to merge 11 commits into
mainfrom
mike/expo-biometrics-package

Conversation

@mikepitre

@mikepitre mikepitre commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Description

Moves biometric credential enrollment, listing, revocation and sign-in out of Clerk's native iOS/Android SDKs and into JS, backed by a new thin native module, @clerk/expo-biometrics. It builds on today's @clerk/expo and its current native client sync. It does not depend on the sync redesign or the @clerk/expo-native-components split, which will rebase on top of this.

This combines #9953, #9959, #9961 and #9960.

@clerk/shared / @clerk/clerk-js: experimental trusted device resources (additive, nothing on the Clerk class changes)

  • trusted_device sign-in strategy: signIn.create({ strategy: 'trusted_device', trustedDeviceId }) and signIn.attemptFirstFactor({ strategy: 'trusted_device', trustedDeviceId, clientData, signature, algorithm: 'ES256' }). The challenge is exposed on signIn.firstFactorVerification.trustedDeviceChallenge (FAPI's expires_at there is in seconds).
  • AuthConfig.nativeSettings.
  • A new BiometricCredential resource, plus User.__experimental_getBiometricCredentials(), __experimental_prepareBiometricCredential(), __experimental_attemptBiometricCredential() and __experimental_revokeBiometricCredential(id) for /v1/me/biometric_credentials.
  • Bundlewatch: the new resources push clerk.native.js and clerk.browser.js over their limits, so their maxSize values go from 80KB to 82KB and from 81KB to 83KB. clerk.browser.js is now 81.1KB gzip.

@clerk/expo-biometrics: new experimental native module (iOS and Android)

It handles only the device side: key creation, ES256 signing behind a biometric prompt, and on-device credential records. It makes no FAPI calls and doesn't depend on clerk-ios or clerk-android.

  • iOS
    • Secure Enclave P-256 keys, with the access-control flags set per policy.
    • Records are kept in the trustedDeviceCredentials keychain item. The item and the reinstall marker follow the clerk-ios storage contract v1 (test: pin biometric credential storage format clerk-ios#584), so credentials created here and by ClerkKit in the same app are interchangeable.
    • On the Simulator, getAvailability() reports secureKeyStorageAvailable: false, because Secure Enclave keys fail there.
  • Android
    • Android Keystore secp256r1 keys, signed through BiometricPrompt.
    • Records live in noBackupFilesDir/clerk/biometric_credentials.v2.json. Writes follow the clerk-android v2 storage contract (feat(api): isolate biometric credential storage clerk-android#966): file lock, atomic writes, unknown fields preserved.
    • Android stores only a SHA-256 of the identifier hint. hashIdentifierHint() and identifierHintSha256 on records let hints match on both platforms.
  • Tests: Swift unit tests and Robolectric tests pin both storage contracts with the same fixtures the native SDKs use.

Why it's a separate package rather than part of @clerk/expo: Expo autolinks every native module in an installed package. Putting this in @clerk/expo would link LocalAuthentication and androidx.biometric into every app, and those apps would have to declare NSFaceIDUsageDescription whether or not they use biometrics. It follows the same pattern as @clerk/expo-passkeys and @clerk/expo-google-signin.

@clerk/expo: useBiometricCredentials() runs in JS

  • @clerk/expo-biometrics is an optional peer dependency, loaded with a guarded require. Without it, the hook's methods throw an error that explains how to install it and rebuild.
  • The orchestration follows clerk-ios BiometricCredentials.swift:
    • Environment gating uses nativeSettings.
    • Local candidates are filtered by app identifier, id and identifier hint. Records whose key is missing are pruned, and records are reconciled with the server list when a session is active.
    • Enrollment runs createKey → prepare → sign → attempt → saveRecord, with the key and server credential cleaned up if a step fails.
    • Sign-in runs signIn.create → sign the challenge → attemptFirstFactor. The local record is dropped when the server or keystore reports it gone.
  • reverify() stays on Clerk's native SDK through the existing ClerkExpo module, and still uses today's sync coordinator (waitForPendingJsToNativeSync / synchronizeNativeClientToJs). FAPI only lists trusted_device reverification factors from API version 2026-08-20, and clerk-js doesn't send that version yet.
  • The native SDK's enroll/list/revoke/sign-in bridge methods in ClerkExpo are left in place, unused by the hook, and will be removed in a follow-up.

Sign-in with Face ID completes in JS through setActive. Apps that render native components receive the session through the existing sync, the same as any other JS sign-in.

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

mikepitre and others added 10 commits September 29, 2026 17:46
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
Implement the Android side of the module against clerk-android's biometric
credential storage contract v2, and add hashIdentifierHint() plus
identifierHintSha256 on listed records on both platforms.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Report secureKeyStorageAvailable from getAvailability() on Android (false
below API 28) and reject createKey() there with
secure_key_storage_unavailable instead of biometry_not_available.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
getAvailability() and signIn() report biometric_authentication_unavailable
before reading local records when @clerk/expo-biometrics reports no secure
key storage, and enroll() maps its secure_key_storage_unavailable rejection
to biometric_authentication_unavailable before contacting Clerk.

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

changeset-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3d8a2eb

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

This PR includes changesets to release 24 packages
Name Type
@clerk/clerk-js Minor
@clerk/shared Minor
@clerk/expo Minor
@clerk/expo-biometrics Minor
@clerk/chrome-extension Patch
@clerk/electron Patch
@clerk/mosaic Patch
@clerk/astro Patch
@clerk/backend Patch
@clerk/expo-passkeys Patch
@clerk/express Patch
@clerk/fastify Patch
@clerk/hono Patch
@clerk/localizations Patch
@clerk/msw Patch
@clerk/nextjs Patch
@clerk/nuxt Patch
@clerk/react-router Patch
@clerk/react Patch
@clerk/swingset Patch
@clerk/tanstack-react-start Patch
@clerk/testing Patch
@clerk/ui Patch
@clerk/vue Patch

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

@vercel

vercel Bot commented Sep 29, 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 30, 2026 1:40pm UTC
swingset Ready Ready Preview Sep 30, 2026 1:40pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 29, 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.

…rify Android storage sharing

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

This branch was successfully deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant