Expo Better Auth Passkey Integration
raw JSON →The `expo-better-auth-passkey` package provides native passkey (WebAuthn) support for applications built with Expo and `better-auth`. It acts as a drop-in replacement for `better-auth`'s standard `passkeyClient`, leveraging platform-specific APIs like Apple's `ASAuthorizationController` for iOS/macOS and Android's Credential Manager. This ensures a consistent passkey experience across web, iOS, and Android using a single codebase. The library, currently at version 1.4.1, maintains an active release schedule with updates addressing bug fixes and new features. Key differentiators include its seamless integration into both managed and bare Expo projects without requiring ejecting, a smart fallback to the web client for web builds, and a TypeScript-first approach with strict type mirroring for robust development.
Common errors
error ASAuthorizationError.Code.notKnown | ASAuthorizationError.Code.canceled | ASAuthorizationError.Code.invalidRequest on iOS ↓
ios.associatedDomains in app.json includes webcredentials:your-rp-id.com and that the apple-app-site-association file is correctly hosted and accessible at https://your-rp-id.com/.well-known/apple-app-site-association. error Passkey operation failed due to invalid relying party ID or origin configuration. ↓
rpID, rpName, and origin values on your Better Auth server configuration with your app's domain. Ensure all client app schemes (e.g., myapp://, https://localhost) are explicitly included in trustedOrigins on the server. error SecurityException: Cannot use Credential Manager without a valid relying party ID configured / 'No Android package hash supplied in WebAuthn attestation options.' ↓
android:apk-key-hash array in your Better Auth server passkey plugin configuration. error TypeError: Cannot read properties of undefined (reading 'passkey') or 'addPasskey' of undefined ↓
expoPasskeyClient() is included in the plugins array passed to createAuthClient({ plugins: [...] }) and that authClient is initialized before attempting passkey operations. Warnings
breaking The parameter structure for `registerPasskey` (now `addPasskey`) and `authenticatePasskey` (now `signIn.passkey`) functions was updated in `v1.2.0`. Projects upgrading from earlier versions must review their usage of these methods. ↓
gotcha For iOS and macOS, the 'Associated Domains' capability must be enabled in your Xcode project or via `expo prebuild` config (`ios.associatedDomains`), and an `apple-app-site-association` file must be correctly hosted on your relying party domain. ↓
gotcha On Android, the Credential Manager APIs used for native passkeys require Google Play Services version 23.30 or newer. Older device versions will not support native passkey operations. ↓
gotcha Your Better Auth server must be configured correctly for passkeys, including `rpID`, `rpName`, `origin`, and crucially, `android:apk-key-hash` entries for Android builds and a comprehensive `trustedOrigins` list for all client entry points. ↓
gotcha Passkey operations fundamentally require a secure origin (HTTPS). Development environments should use tunneling services like ngrok or localhost HTTPS setup to avoid security errors. ↓
gotcha A bug fix in v1.4.1 addressed an issue where `presentationAnchor` was not reliably running on the main thread, which could lead to UI freezes or native API call failures on older versions. ↓
Install
npm install expo-better-auth-passkey yarn add expo-better-auth-passkey pnpm add expo-better-auth-passkey Imports
- expoPasskeyClient wrong
const expoPasskeyClient = require('expo-better-auth-passkey');correctimport { expoPasskeyClient } from 'expo-better-auth-passkey'; - createAuthClient wrong
import createAuthClient from 'better-auth/react';correctimport { createAuthClient } from 'better-auth/react'; - PasskeyPlugin wrong
import { PasskeyPlugin } from '@better-auth/passkey';correctimport type { PasskeyPlugin } from '@better-auth/passkey';
Quickstart
import { createAuthClient } from 'better-auth/react';
import { expoPasskeyClient } from 'expo-better-auth-passkey';
import { Platform } from 'react-native';
// Replace with your actual backend URL where Better Auth server is running
const BASE_URL = process.env.BETTER_AUTH_API_URL || 'https://your-api.mydomain.com';
// Initialize the Better Auth client with the Expo Passkey plugin
export const authClient = createAuthClient({
baseURL: BASE_URL,
plugins: [
expoPasskeyClient(),
// Add other Better Auth client plugins as needed (e.g., email, session)
],
});
async function handlePasskeyOperations() {
try {
console.log(`Running on platform: ${Platform.OS}`);
// Example: Register a new passkey with a descriptive name
const addPasskeyResult = await authClient.passkey.addPasskey({ name: `My ${Platform.OS} Passkey` });
console.log('Passkey registered successfully:', addPasskeyResult);
// Example: Sign in using an existing passkey
// In a real application, you might first get the user's identifier (e.g., email)
// before attempting passkey sign-in.
const signInResult = await authClient.signIn.passkey({ email: 'user@example.com' });
console.log('Signed in with passkey successfully:', signInResult);
} catch (error) {
console.error('Passkey operation failed:', error);
// Implement robust error handling, e.g., user cancellation, network issues,
// or specific platform errors related to passkey APIs.
}
}
// Execute the passkey operations
handlePasskeyOperations();