Passkey
Add passwordless passkey sign-in and device management to your authentication flow.
The passkey plugin adds passwordless authentication using WebAuthn. Users can sign in with their device authenticator (Touch ID, Face ID, Windows Hello) and manage registered passkeys from their security settings.
It contributes:
- A "Continue with Passkey" button rendered on the sign-in and magic-link views (hidden on sign-up)
- A
<Passkeys />security card for listing, adding, renaming, and deleting registered passkeys useSignInPasskey,useAddPasskey,useUpdatePasskey,useDeletePasskey, anduseListPasskeyshooks
Setup
Install the Better Auth plugin
Install the @better-auth/passkey package and add it to your Better Auth server config:
import { betterAuth } from "better-auth"
import { passkey } from "@better-auth/passkey"
export const auth = betterAuth({
// ...
plugins: [
passkey()
]
})Install the matching client plugin
Add passkeyClient() to your auth client so authClient.signIn.passkey and authClient.passkey.* are available:
import { createAuthClient } from "better-auth/react"
import { passkeyClient } from "@better-auth/passkey/client"
export const authClient = createAuthClient({
plugins: [passkeyClient()]
})Install the UI plugin
Run the shadcn CLI to install the passkey button, passkey management card, and the passkeyPlugin() factory into your project:
npx shadcn@latest add @better-auth-ui/passkeyThis drops the following into your codebase:
src/lib/auth/auth-plugin.ts: localAuthPlugintyping widenersrc/lib/auth/passkey-plugin.ts:passkeyPlugin()factorysrc/components/auth/passkey/passkey-button.tsx: the "Continue with Passkey" sign-in buttonsrc/components/auth/passkey/passkeys.tsx: the passkey management cardsrc/components/auth/passkey/passkey.tsx: individual passkey rowsrc/components/auth/passkey/passkey-skeleton.tsx: skeleton shown while passkeys loadsrc/components/auth/passkey/passkeys-empty.tsx: empty state shown when no passkeys existsrc/components/auth/passkey/add-passkey-dialog.tsx: dialog for registering a new passkeysrc/components/auth/passkey/delete-passkey-dialog.tsx: confirmation dialog for revoking a passkey
Register the plugin
Pass passkeyPlugin() to <AuthProvider>:
import { passkeyPlugin } from "@/lib/auth/passkey-plugin"
import { AuthProvider } from "@/components/auth/auth-provider"
<AuthProvider
authClient={authClient}
navigate={navigate}
plugins={[passkeyPlugin()]}
>
{children}
</AuthProvider>Components
<SignIn />
A "Continue with Passkey" button is automatically rendered on the <SignIn /> and <MagicLink /> views when the plugin is registered (hidden on sign-up).
Usage
import { PasskeyButton } from "@/components/auth/passkey/passkey-button"
<PasskeyButton />Props
<Passkeys />
Passkeys
The security settings page shows <Passkeys /> when the layout renders plugin securityCards. Add passkeyPlugin() to plugins to provide this card.
Usage
import { Passkeys } from "@/components/auth/passkey/passkeys"
<Passkeys />Props
Prop
Type
Passkey registration policy
By default, the add-passkey dialog does not set authenticatorAttachment.
The browser and operating system show the available passkey options.
Set a preference in the plugin when all registrations must use one authenticator type:
passkeyPlugin({ authenticatorAttachment: "platform" })
passkeyPlugin({ authenticatorAttachment: "cross-platform" })"platform" prefers the current device. "cross-platform" prefers a security key or another device. The dialog does not show an attachment selector.
useAddPasskey accepts every parameter exposed by authClient.passkey.addPasskey. Use this hook for custom registration flows:
const { mutate: addPasskey } = useAddPasskey(
authClient as PasskeyAuthClient
)
addPasskey({
name: "Work laptop",
authenticatorAttachment: "platform",
extensions: { credProps: true },
returnWebAuthnResponse: true
})residentKey and userVerification are server plugin policies. They are not
parameters of authClient.passkey.addPasskey, so the client UI does not expose
them.
Passkey autofill
With the plugin registered, the sign-in form asks the browser to offer saved passkeys straight from its autofill dropdown, so most people never press the passkey button at all. This is the WebAuthn conditional UI flow.
Two pieces make it work, and the built-in components already handle both:
- The identifier and password fields carry
webauthnas the last token of theirautocompleteattribute, added bywithPasskeyAutoFill. <PasskeyButton />callsusePasskeyAutoFill, which opens a conditionally mediated request once the browser reports that it supports one.
Browsers without conditional mediation ignore the extra token and never get the request, so the button stays as the fallback everywhere.
Turn the whole thing off with:
passkeyPlugin({ autoFill: false })If you write your own sign-in form, add the token and start the conditional request:
import type { PasskeyAuthClient } from "@better-auth-ui/core/plugins/passkey"
import {
isPasskeyAutoFillEnabled,
withPasskeyAutoFill
} from "@better-auth-ui/core/plugins/passkey"
import { usePasskeyAutoFill } from "@better-auth-ui/react/plugins/passkey"
const { authClient, plugins } = useAuth()
const passkeyAutoFill = isPasskeyAutoFillEnabled(plugins)
usePasskeyAutoFill(authClient as PasskeyAuthClient)
<Input autoComplete={withPasskeyAutoFill("email", passkeyAutoFill)} />navigator.credentials.get() accepts an AbortSignal. The bundled hook calls
the Better Auth passkey client, which does not expose that signal. Unmounting
the form only stops a pending availability probe. It does not cancel a request
that already started.
If your custom implementation calls navigator.credentials.get() directly,
pass an AbortSignal and abort it during cleanup.
Options
passkeyPlugin({
// Omit this option to let the browser show all available choices.
authenticatorAttachment: "platform",
// Override any of the plugin's localization strings.
localization: {
passkeys: "Security Keys"
}
})Prop
Type
Localization
Prop
Type
Last updated on