{"id":16726,"library":"expo-passkey","title":"Expo Passkey","description":"Expo Passkey is a cross-platform (iOS, Android, Web) module designed for Expo applications, providing comprehensive FIDO2/WebAuthn passkey authentication. It seamlessly integrates with the Better Auth ecosystem, offering an end-to-end solution for modern, frictionless biometric and platform authenticator-based login experiences. Currently at version 0.3.12, the package maintains an active development pace, demonstrated by recent critical security patches and breaking changes introduced in October 2025. Key differentiators include its unified passkey table structure that operates consistently across all supported platforms, seamless synchronization with iCloud Keychain and Google Password Manager for cross-device portability, and granular client-controlled WebAuthn security preferences, all built upon a robust framework emphasizing session-validated security for critical operations like registration and revocation.","status":"active","version":"0.3.12","language":"javascript","source_language":"en","source_url":"https://github.com/iosazee/expo-passkey","tags":["javascript","react-native","expo","expo-passkey","ExpoPasskeyModule","passkey","biometric","authentication","better-auth","typescript"],"install":[{"cmd":"npm install expo-passkey","lang":"bash","label":"npm"},{"cmd":"yarn add expo-passkey","lang":"bash","label":"yarn"},{"cmd":"pnpm add expo-passkey","lang":"bash","label":"pnpm"}],"dependencies":[{"reason":"Core dependency for any Expo module.","package":"expo","optional":false},{"reason":"Fundamental dependency for React Native applications.","package":"react","optional":false},{"reason":"Fundamental dependency for React Native applications.","package":"react-native","optional":false},{"reason":"Core integration for the Better Auth backend services and client-side utilities.","package":"@better-auth/expo","optional":false},{"reason":"Core integration for the Better Auth backend services.","package":"better-auth","optional":false},{"reason":"While expo-passkey is client-side, a complete passkey solution requires a server-side component for challenge generation and verification, often leveraging this library. The package's ecosystem implies its necessity.","package":"@simplewebauthn/server","optional":false},{"reason":"Used for securely storing sensitive data on the device, often relevant for authentication flows.","package":"expo-secure-store","optional":false},{"reason":"Enables biometric authentication (Face ID, Touch ID, fingerprint) on native platforms.","package":"expo-local-authentication","optional":false},{"reason":"Used for schema validation, likely for API responses and internal data structures.","package":"zod","optional":false}],"imports":[{"note":"Expo Passkey ships as an ES Module (ESM) and is TypeScript-ready. CommonJS `require` syntax will not work.","wrong":"const { registerPasskey } = require('expo-passkey');","symbol":"registerPasskey, authenticatePasskey, revokePasskey","correct":"import { registerPasskey, authenticatePasskey, revokePasskey } from 'expo-passkey';"},{"note":"A named export providing utilities like `isSupported()` for checking passkey availability. Do not use as a default import.","wrong":"import PasskeyStatus from 'expo-passkey';","symbol":"PasskeyStatus","correct":"import { PasskeyStatus } from 'expo-passkey';"},{"note":"These are TypeScript type definitions. Use `import type` to avoid bundling unnecessary runtime code, especially in environments that don't strip type imports.","wrong":"import { RegisterPasskeyOptions, AuthenticatePasskeyOptions } from 'expo-passkey';","symbol":"RegisterPasskeyOptions, AuthenticatePasskeyOptions","correct":"import type { RegisterPasskeyOptions, AuthenticatePasskeyOptions } from 'expo-passkey';"}],"quickstart":{"code":"import { registerPasskey, authenticatePasskey, PasskeyStatus } from 'expo-passkey';\nimport * as WebBrowser from 'expo-web-browser';\nimport { Platform } from 'react-native';\n\nconst API_BASE_URL = 'https://your-backend.com/api'; // Replace with your backend URL\n\nasync function handlePasskeyRegistration(userId: string) {\n  try {\n    console.log('Initiating passkey registration...');\n    const response = await fetch(`${API_BASE_URL}/passkey/register/start`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({ userId }), // Send userId to backend for challenge generation\n    });\n    const { challengeOptions } = await response.json();\n\n    const registrationResult = await registerPasskey({\n      challenge: challengeOptions,\n      openWebBrowserAsync: Platform.OS === 'web' ? WebBrowser.openBrowserAsync : undefined,\n    });\n\n    const verificationResponse = await fetch(`${API_BASE_URL}/passkey/register/complete`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify(registrationResult),\n    });\n\n    if (verificationResponse.ok) {\n      console.log('Passkey registered successfully!');\n      return true;\n    } else {\n      const error = await verificationResponse.json();\n      console.error('Passkey registration failed on server:', error);\n      return false;\n    }\n  } catch (error) {\n    console.error('Error during passkey registration:', error);\n    return false;\n  }\n}\n\nasync function handlePasskeyAuthentication() {\n  try {\n    console.log('Initiating passkey authentication...');\n    const response = await fetch(`${API_BASE_URL}/passkey/authenticate/start`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      // No userId sent for auth challenge since v0.3.6\n    });\n    const { challengeOptions } = await response.json();\n\n    const authenticationResult = await authenticatePasskey({\n      challenge: challengeOptions,\n      openWebBrowserAsync: Platform.OS === 'web' ? WebBrowser.openBrowserAsync : undefined,\n    });\n\n    const verificationResponse = await fetch(`${API_BASE_URL}/passkey/authenticate/complete`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify(authenticationResult),\n    });\n\n    if (verificationResponse.ok) {\n      const { user } = await verificationResponse.json(); // Backend might return user info\n      console.log('Passkey authenticated successfully for user:', user?.id || 'unknown');\n      return user;\n    } else {\n      const error = await verificationResponse.json();\n      console.error('Passkey authentication failed on server:', error);\n      return null;\n    }\n  } catch (error) {\n    console.error('Error during passkey authentication:', error);\n    return null;\n  }\n}\n\n// Example of checking passkey support (e.g., in a useEffect hook)\n// async function checkPasskeySupport() {\n//   const supported = await PasskeyStatus.isSupported();\n//   console.log('Passkey authentication supported:', supported);\n// }\n// checkPasskeySupport();","lang":"typescript","description":"This quickstart demonstrates the client-side flow for registering and authenticating a passkey in an Expo application. It illustrates the typical three-step process: obtaining a challenge from a backend server, performing the client-side WebAuthn operation, and sending the result back to the server for verification, including considerations for web platform support."},"warnings":[{"fix":"Update client-side calls to `revokePasskey()` by removing the `userId` parameter. Ensure your application establishes an authenticated user session before initiating passkey registration or revocation. Your backend must derive the `userId` for these operations solely from the authenticated session, not from client-provided data.","message":"The `revokePasskey()` function no longer accepts a `userId` parameter. Additionally, both registration and revocation operations now strictly require an authenticated session on your backend. Attempting these operations without a valid session will result in a `401 Unauthorized` error.","severity":"breaking","affected_versions":">=0.3.0"},{"fix":"Immediately upgrade `expo-passkey` to `v0.3.0` or a later version. On your backend, modify all passkey-related endpoints to ensure the `userId` is obtained and validated *only* from the authenticated user's session, disallowing `userId` from client request bodies for security-sensitive actions.","message":"A critical account takeover vulnerability was present in versions prior to `v0.3.0`. The server was validating the `userId` from client requests for critical operations, which could be manipulated. This has been patched to enforce server-side validation of `userId` exclusively from the authenticated session.","severity":"breaking","affected_versions":"<0.3.0"},{"fix":"Upgrade `expo-passkey` to `v0.3.6` or later. Verify that your backend's authentication challenge endpoint (e.g., `/passkey/authenticate/start`) is configured to *not* require a pre-existing authenticated session, allowing any user to request a challenge.","message":"In versions prior to `v0.3.6`, authentication challenge endpoints sometimes incorrectly failed with a `401 Unauthorized` status for unauthenticated users. This bug prevented a smooth login flow where a user might initiate authentication without being logged in first.","severity":"gotcha","affected_versions":"<0.3.6"},{"fix":"Carefully review the `peerDependencies` listed in `expo-passkey`'s `package.json`. Ensure all required packages are installed in your project at compatible versions. For example, run `npm install @better-auth/expo better-auth @simplewebauthn/server expo expo-application expo-crypto expo-device expo-local-authentication expo-secure-store react react-native zod` and verify versions match.","message":"`expo-passkey` relies on a substantial list of peer dependencies, including core Expo modules (e.g., `expo-secure-store`, `expo-local-authentication`), `better-auth` components, `@simplewebauthn/server` (for server integration), and `zod`. Missing or incompatible versions of these dependencies can lead to runtime errors, build failures, or unexpected behavior.","severity":"gotcha","affected_versions":"*"}],"env_vars":null,"last_verified":"2026-04-22T00:00:00.000Z","next_check":"2026-07-21T00:00:00.000Z","problems":[{"fix":"Upgrade `expo-passkey` to `v0.3.6` or later, and ensure your server-side endpoint for initiating passkey authentication does not enforce a prior user session.","cause":"The backend's authentication challenge endpoint is incorrectly requiring an authenticated session before `v0.3.6`.","error":"401 Unauthorized error when attempting passkey authentication, even for unauthenticated users."},{"fix":"Upgrade `expo-passkey` to `v0.3.0` or higher. On the backend, always obtain `userId` for passkey registration and revocation from the authenticated user's session, ignoring any `userId` sent from the client.","cause":"Server-side code is accepting `userId` from client requests for sensitive operations, making it susceptible to manipulation (pre-`v0.3.0`).","error":"Security vulnerabilities or 'Invalid userId' warnings reported for passkey registration/revocation."},{"fix":"Remove the `userId` parameter from `revokePasskey` calls in your client-side code. Ensure an authenticated session is established before calling `revokePasskey`.","cause":"Attempting to pass the `userId` parameter to `revokePasskey` after it was removed in `v0.3.0`.","error":"TypeError: revokePasskey is not a function or Argument of type '{ userId: string; credentialId: string; }' is not assignable to parameter of type '{ credentialId: string; }'."},{"fix":"Install all specified peer dependencies manually using your package manager (e.g., `npm install @better-auth/expo expo-secure-store @simplewebauthn/server`). Check the `package.json` for exact version requirements and ensure compatibility.","cause":"A required peer dependency, such as `@better-auth/expo`, `expo-secure-store`, or `@simplewebauthn/server`, is either not installed or its version is incompatible.","error":"Module not found: Can't resolve '@better-auth/expo' in '...' or similar peer dependency resolution errors."}],"ecosystem":"npm"}