mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-19 23:53:17 -05:00
406 lines
13 KiB
Plaintext
406 lines
13 KiB
Plaintext
---
|
|
title: Passkey
|
|
description: Passkey
|
|
---
|
|
|
|
Passkeys are a secure, passwordless authentication method using cryptographic key pairs, supported by WebAuthn and FIDO2 standards in web browsers. They replace passwords with unique key pairs: a private key stored on the user's device and a public key shared with the website. Users can log in using biometrics, PINs, or security keys, providing strong, phishing-resistant authentication without traditional passwords.
|
|
|
|
The passkey plugin implementation is powered by [SimpleWebAuthn](https://simplewebauthn.dev/) behind the scenes.
|
|
|
|
## Installation
|
|
|
|
<Steps>
|
|
<Step>
|
|
### Install the plugin
|
|
|
|
```package-install
|
|
npm install @better-auth/passkey
|
|
```
|
|
</Step>
|
|
<Step>
|
|
### Add the plugin to your auth config
|
|
To add the passkey plugin to your auth config, you need to import the plugin and pass it to the `plugins` option of the auth instance.
|
|
|
|
```ts title="auth.ts"
|
|
import { betterAuth } from "better-auth"
|
|
import { passkey } from "@better-auth/passkey" // [!code highlight]
|
|
|
|
export const auth = betterAuth({
|
|
plugins: [
|
|
passkey(), // [!code highlight]
|
|
],
|
|
})
|
|
```
|
|
</Step>
|
|
<Step>
|
|
### Migrate the database
|
|
|
|
Run the migration or generate the schema to add the necessary fields and tables to the database.
|
|
|
|
<Tabs items={["migrate", "generate"]}>
|
|
<Tab value="migrate">
|
|
```package-install
|
|
npx auth migrate
|
|
```
|
|
</Tab>
|
|
<Tab value="generate">
|
|
```package-install
|
|
npx auth generate
|
|
```
|
|
</Tab>
|
|
</Tabs>
|
|
See the [Schema](#schema) section to add the fields manually.
|
|
</Step>
|
|
<Step>
|
|
### Add the client plugin
|
|
|
|
```ts title="auth-client.ts"
|
|
import { createAuthClient } from "better-auth/client"
|
|
import { passkeyClient } from "@better-auth/passkey/client" // [!code highlight]
|
|
|
|
export const authClient = createAuthClient({
|
|
plugins: [
|
|
passkeyClient() // [!code highlight]
|
|
]
|
|
})
|
|
```
|
|
</Step>
|
|
|
|
</Steps>
|
|
|
|
## Usage
|
|
|
|
### Add/Register a passkey
|
|
|
|
To add or register a passkey make sure a user is authenticated and then call the `passkey.addPasskey` function provided by the client.
|
|
|
|
<APIMethod path="/passkey/add-passkey" method="POST" isClientOnly>
|
|
```ts
|
|
type addPasskey = {
|
|
/**
|
|
* An optional name to label the authenticator account being registered. If not provided, it will default to the user's email address or user ID
|
|
*/
|
|
name?: string = "example-passkey-name"
|
|
/**
|
|
* You can also specify the type of authenticator you want to register. Default behavior allows both platform and cross-platform passkeys
|
|
*/
|
|
authenticatorAttachment?: "platform" | "cross-platform" = "cross-platform"
|
|
}
|
|
```
|
|
</APIMethod>
|
|
|
|
<Callout>
|
|
Setting `throw: true` in the fetch options has no effect for the register and sign-in passkey responses — they will always return a data object containing the error object.
|
|
</Callout>
|
|
|
|
|
|
### Sign in with a passkey
|
|
|
|
To sign in with a passkey you can use the `signIn.passkey` method. This will prompt the user to sign in with their passkey.
|
|
|
|
|
|
<APIMethod path="/sign-in/passkey" method="POST" isClientOnly>
|
|
```ts
|
|
type signInPasskey = {
|
|
/**
|
|
* Browser autofill, a.k.a. Conditional UI. Read more: https://simplewebauthn.dev/docs/packages/browser#browser-autofill-aka-conditional-ui
|
|
*/
|
|
autoFill?: boolean = true
|
|
}
|
|
```
|
|
</APIMethod>
|
|
|
|
#### Example Usage
|
|
```ts
|
|
import { authClient } from "@/lib/auth-client";
|
|
|
|
// With post authentication redirect
|
|
await authClient.signIn.passkey({
|
|
autoFill: true,
|
|
fetchOptions: {
|
|
onSuccess(context) {
|
|
// Redirect to dashboard after successful authentication
|
|
window.location.href = "/dashboard";
|
|
},
|
|
onError(context) {
|
|
// Handle authentication errors
|
|
console.error("Authentication failed:", context.error.message);
|
|
}
|
|
}
|
|
});
|
|
```
|
|
|
|
### List passkeys
|
|
|
|
You can list all of the passkeys for the authenticated user by calling `passkey.listUserPasskeys`:
|
|
|
|
<APIMethod
|
|
path="/passkey/list-user-passkeys"
|
|
method="GET"
|
|
requireSession
|
|
resultVariable="passkeys"
|
|
>
|
|
```ts
|
|
type listPasskeys = {
|
|
}
|
|
```
|
|
</APIMethod>
|
|
|
|
### Deleting passkeys
|
|
|
|
You can delete a passkey by calling `passkey.delete` and providing the passkey ID.
|
|
|
|
<APIMethod
|
|
path="/passkey/delete-passkey"
|
|
method="POST"
|
|
requireSession
|
|
>
|
|
```ts
|
|
type deletePasskey = {
|
|
/**
|
|
* The ID of the passkey to delete.
|
|
*/
|
|
id: string = "some-passkey-id"
|
|
}
|
|
```
|
|
</APIMethod>
|
|
|
|
### Updating passkey names
|
|
|
|
<APIMethod
|
|
path="/passkey/update-passkey"
|
|
method="POST"
|
|
requireSession
|
|
>
|
|
```ts
|
|
type updatePasskey = {
|
|
/**
|
|
* The ID of the passkey which you want to update.
|
|
*/
|
|
id: string = "id of passkey"
|
|
/**
|
|
* The new name which the passkey will be updated to.
|
|
*/
|
|
name: string = "my-new-passkey-name"
|
|
}
|
|
```
|
|
</APIMethod>
|
|
|
|
### Conditional UI
|
|
|
|
The plugin supports conditional UI, which allows the browser to autofill the passkey if the user has already registered a passkey.
|
|
|
|
There are two requirements for conditional UI to work:
|
|
|
|
<Steps>
|
|
<Step>
|
|
#### Update input fields
|
|
|
|
Add the `autocomplete` attribute with the value `webauthn` to your input fields. You can add this attribute to multiple input fields, but at least one is required for conditional UI to work.
|
|
|
|
The `webauthn` value should also be the last entry of the `autocomplete` attribute.
|
|
|
|
```html
|
|
<label for="name">Username:</label>
|
|
<input type="text" name="name" autocomplete="username webauthn">
|
|
<label for="password">Password:</label>
|
|
<input type="password" name="password" autocomplete="current-password webauthn">
|
|
```
|
|
</Step>
|
|
<Step>
|
|
#### Preload the passkeys
|
|
|
|
When your component mounts, you can preload the user's passkeys by calling the `authClient.signIn.passkey` method with the `autoFill` option set to `true`.
|
|
|
|
To prevent unnecessary calls, we will also add a check to see if the browser supports conditional UI.
|
|
|
|
<Tabs items={["React"]}>
|
|
<Tab value="React">
|
|
```ts
|
|
useEffect(() => {
|
|
if (!PublicKeyCredential.isConditionalMediationAvailable ||
|
|
!PublicKeyCredential.isConditionalMediationAvailable()) {
|
|
return;
|
|
}
|
|
|
|
void authClient.signIn.passkey({ autoFill: true })
|
|
}, [])
|
|
```
|
|
</Tab>
|
|
</Tabs>
|
|
|
|
</Step>
|
|
|
|
</Steps>
|
|
|
|
Depending on the browser, a prompt will appear to autofill the passkey. If the user has multiple passkeys, they can select the one they want to use.
|
|
|
|
Some browsers also require the user to first interact with the input field before the autofill prompt appears.
|
|
|
|
### Debugging
|
|
|
|
To test your passkey implementation you can use [emulated authenticators](https://developer.chrome.com/docs/devtools/webauthn). This way you can test the registration and sign-in process without even owning a physical device.
|
|
|
|
## Schema
|
|
|
|
The plugin require a new table in the database to store passkey data.
|
|
|
|
Table Name: `passkey`
|
|
|
|
<DatabaseTable
|
|
fields={[
|
|
{
|
|
name: "id",
|
|
type: "string",
|
|
description: "Unique identifier for each passkey",
|
|
isPrimaryKey: true
|
|
},
|
|
{
|
|
name: "name",
|
|
type: "string",
|
|
description: "The name of the passkey",
|
|
isOptional: true
|
|
},
|
|
{
|
|
name: "publicKey",
|
|
type: "string",
|
|
description: "The public key of the passkey",
|
|
},
|
|
{
|
|
name: "userId",
|
|
type: "string",
|
|
description: "The ID of the user",
|
|
isForeignKey: true
|
|
},
|
|
{
|
|
name: "credentialID",
|
|
type: "string",
|
|
description: "The unique identifier of the registered credential",
|
|
},
|
|
{
|
|
name: "counter",
|
|
type: "number",
|
|
description: "The counter of the passkey",
|
|
},
|
|
{
|
|
name: "deviceType",
|
|
type: "string",
|
|
description: "The type of device used to register the passkey",
|
|
},
|
|
{
|
|
name: "backedUp",
|
|
type: "boolean",
|
|
description: "Whether the passkey is backed up",
|
|
},
|
|
{
|
|
name: "transports",
|
|
type: "string",
|
|
description: "The transports used to register the passkey",
|
|
isOptional: true
|
|
},
|
|
{
|
|
name: "createdAt",
|
|
type: "Date",
|
|
description: "The time when the passkey was created",
|
|
isOptional: true
|
|
},
|
|
{
|
|
name: "aaguid",
|
|
type: "string",
|
|
description: "Authenticator's Attestation GUID indicating the type of the authenticator",
|
|
isOptional: true
|
|
},
|
|
]}
|
|
/>
|
|
|
|
## Options
|
|
|
|
**rpID**: A unique identifier for your website based on your auth server origin.
|
|
`'localhost'` is okay for local dev. RP ID can be formed by discarding zero or more labels from the left of its effective domain
|
|
until it hits an effective TLD. So `www.example.com` can use the RP IDs `www.example.com` or `example.com`. But not `com`, because that's an eTLD.
|
|
|
|
**rpName**: Human-readable title for your website.
|
|
|
|
**origin**: The origin URL at which your better-auth server is hosted. `http://localhost` and `http://localhost:PORT` are also valid. Do NOT include any trailing /.
|
|
|
|
**authenticatorSelection**: Allows customization of WebAuthn authenticator selection criteria. Leave unspecified for default settings.
|
|
|
|
- `authenticatorAttachment`: Specifies the type of authenticator
|
|
- `platform`: Authenticator is attached to the platform (e.g., fingerprint reader)
|
|
- `cross-platform`: Authenticator is not attached to the platform (e.g., security key)
|
|
- Default: `not set` (both platform and cross-platform allowed, with platform preferred)
|
|
- `residentKey`: Determines credential storage behavior.
|
|
- `required`: User MUST store credentials on the authenticator (highest security)
|
|
- `preferred`: Encourages credential storage but not mandatory
|
|
- `discouraged`: No credential storage required (fastest experience)
|
|
- Default: `preferred`
|
|
- `userVerification`: Controls biometric/PIN verification during authentication:
|
|
- `required`: User MUST verify identity (highest security)
|
|
- `preferred`: Verification encouraged but not mandatory
|
|
- `discouraged`: No verification required (fastest experience)
|
|
- Default: `preferred`
|
|
|
|
**advanced**: Advanced options
|
|
|
|
- `webAuthnChallengeCookie`: Cookie name for storing WebAuthn challenge ID during authentication flow (Default: `better-auth-passkey`)
|
|
|
|
## Expo Integration
|
|
|
|
When using the passkey plugin with Expo, you need to configure the `cookiePrefix` option in the Expo client to ensure passkey cookies are properly detected and stored.
|
|
|
|
By default, the passkey plugin uses `"better-auth-passkey"` as the challenge cookie name. Since this starts with `"better-auth"`, it will work with the default Expo client configuration. However, if you customize the `webAuthnChallengeCookie` option, you must also update the `cookiePrefix` in your Expo client configuration.
|
|
|
|
### Example Configuration
|
|
|
|
If you're using a custom cookie name:
|
|
|
|
```ts title="Server: auth.ts"
|
|
import { betterAuth } from "better-auth";
|
|
import { passkey } from "@better-auth/passkey";
|
|
|
|
export const auth = betterAuth({
|
|
plugins: [
|
|
passkey({
|
|
advanced: {
|
|
webAuthnChallengeCookie: "my-app-passkey" // Custom cookie name
|
|
}
|
|
})
|
|
]
|
|
});
|
|
```
|
|
|
|
Make sure to configure your Expo client with the matching prefix:
|
|
|
|
```ts title="Client: auth-client.ts"
|
|
import { createAuthClient } from "better-auth/react";
|
|
import { expoClient } from "@better-auth/expo/client";
|
|
import { passkeyClient } from "@better-auth/passkey/client";
|
|
import * as SecureStore from "expo-secure-store";
|
|
|
|
export const authClient = createAuthClient({
|
|
baseURL: "http://localhost:8081",
|
|
plugins: [
|
|
expoClient({
|
|
storage: SecureStore,
|
|
cookiePrefix: "my-app" // Must match the prefix of your custom cookie name
|
|
}),
|
|
passkeyClient()
|
|
]
|
|
});
|
|
```
|
|
|
|
If you're using multiple authentication systems or custom cookie names, you can provide an array of prefixes:
|
|
|
|
```ts title="Client: auth-client.ts"
|
|
expoClient({
|
|
storage: SecureStore,
|
|
cookiePrefix: ["better-auth", "my-app", "custom-auth"]
|
|
})
|
|
```
|
|
|
|
<Callout type="warn">
|
|
If the `cookiePrefix` doesn't match the prefix of your `webAuthnChallengeCookie`, the passkey authentication flow will fail because the challenge cookie won't be stored and sent back to the server during verification.
|
|
</Callout>
|
|
|
|
For more information on Expo integration, see the [Expo documentation](/docs/integrations/expo).
|