Skip to content

feat(expo-biometrics): add Android support - #9961

Draft
mikepitre wants to merge 2 commits into
mike/expo-biometrics-iosfrom
mike/expo-biometrics-android
Draft

mikepitre wants to merge 2 commits into
mike/expo-biometrics-iosfrom
mike/expo-biometrics-android

Conversation

@mikepitre

@mikepitre mikepitre commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Description

Implements the Android side of @clerk/expo-biometrics, which was a not_implemented stub in #9959. It follows clerk-android's biometric credential storage contract v2 (clerk/clerk-android#966), so credentials enrolled by this module and by clerk-android in the same app are interchangeable. The module does not depend on clerk-android.

  • Keys: Android Keystore EC secp256r1 signing keys with the alias com.clerk.trusted_device.<localKeyId>, where localKeyId is tdlk_ + 32 hex characters. Every use requires user authentication. On API 30+ the authenticators are set per policy, with invalidatedByBiometricEnrollment only for biometry_current_set. API 28–29 use validity -1, and on API < 28 getAvailability() reports secureKeyStorageAvailable: false, createKey() rejects with secure_key_storage_unavailable, and sign() with biometry_not_available.

  • Signing: androidx BiometricPrompt with a CryptoObject(Signature) on the current FragmentActivity. The DER signature is converted to raw r || s and encoded as unpadded base64url. KeyPermanentlyInvalidatedException maps to key_invalidated. The prompt allows a device credential only when the key itself accepts one (from KeyInfo on API 30+). For keys created on API 30+ this matches the contract's per-policy rule. It also keeps biometry_or_device_passcode keys created on API 28–29 biometric-only after an OS upgrade, since a PIN could not unlock them.

  • Records: stored in the contract's plaintext noBackupFilesDir/clerk/biometric_credentials.v2.json (version 2, snake_case fields, only identifier_hint_sha256). Every read-modify-write goes through the contract's write protocol:

    • an in-process mutex, then FileChannel.tryLock on biometric_credentials.lock, retrying OverlappingFileLockException/null with a 2 → 50 ms backoff for up to 5 s
    • the lock channel stays open for the life of the process
    • atomic temp-file writes with an fsync, a rename, and a best-effort directory fsync
    • unknown fields, unknown policies, undecodable records and unknown top-level keys are preserved; the pending cleanup queue is left untouched
    • v1 data and clerk_preferences are never read or written

    The bridge converts between the snake_case storage fields and the existing camelCase record shape.

  • Other functions:

    • getAppIdentifier() returns the package name.
    • getAvailability() uses BiometricManager.canAuthenticate and reports biometryType: 'biometric' (new union member), since Android does not say which sensor is a strong biometric.
    • ensureInstallationMarker() always resolves { wiped: false }, because uninstalling wipes both noBackupFilesDir and the app's Keystore keys.
    • saveRecord(..., { removeOtherRecordsForApp: true }) deletes only the same user's other records, as contract section 5.3 requires.

Android stores only hashed identifier hints, so this also adds a cross-platform API:

  • hashIdentifierHint(hint) (sync) returns the lowercase hex SHA-256 of the trimmed, lowercased hint, or null when it is empty.
  • listRecords() records now include identifierHintSha256 on both platforms. iOS computes it from its stored raw hint and still returns identifierHint. Android returns identifierHint: null.
  • saveRecord still takes the raw identifierHint. iOS stores it raw (clerk-ios contract v1), and Android stores only its hash.

Robolectric unit tests are wired into the module's Gradle test task. They pin the contract with the v2 fixture copied from clerk/clerk-android#966.

Stacked on #9959.

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

@changeset-bot

changeset-bot Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9ab9910

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

@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

@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 and others added 2 commits September 28, 2026 16:10
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>

This branch was successfully deployed

2 active deployments
Preview – swingset — 9ab99108 Deployed Sep 28, 2026 by vercel[bot]
Preview – clerk-js-sandbox — 9ab99108 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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant