Passkey
WebAuthn / FIDO2 passwordless authentication with passkeys.
The PasskeyPlugin enables passwordless authentication using the WebAuthn standard. Users can register passkeys (biometric, security keys, etc.) and use them to sign in without a password.
Setup
This example assumes you've already defined AppAuthSchema. If you want a complete setup from entity definitions through Axum mounting, start with the Axum integration guide.
use better_auth::plugins::PasskeyPlugin;
let auth = BetterAuth::<AppAuthSchema>::new(config)
.store(store)
.plugin(
PasskeyPlugin::new()
.rp_id("example.com")
.rp_name("My Application")
.origin("https://example.com")
)
.build()
.await?;Configuration
| Option | Type | Default | Description |
|---|---|---|---|
rp_id | String | base_url host | Relying Party ID (your domain) |
rp_name | String | "Better Auth" | Relying Party display name |
origin | String | request Origin header | Expected origin for WebAuthn responses |
challenge_ttl_secs | i64 | 300 (5 min) | Challenge expiration in seconds |
rp_id must be an effective domain of origin. The plugin rejects mismatched values before generating a challenge.
The plugin follows Better Auth's userVerification: "preferred" behavior. Registration and authentication require user presence, but do not require the authenticator's user-verification flag. An authenticator may use a PIN or biometrics without making verification mandatory for subsequent authentication. Challenge, origin, RP ID, signature, and counter checks still apply. Each verification attempt consumes its challenge, including failed attempts.
How It Works
Registration Flow
- Client calls
GET /passkey/generate-register-optionsto get WebAuthn creation options - Client uses the browser's
navigator.credentials.create()API with the options - Client sends the credential response to
POST /passkey/verify-registration - Server validates the WebAuthn registration response and stores the passkey
Authentication Flow
- Client calls
GET /passkey/generate-authenticate-optionsto get assertion options - Client uses the browser's
navigator.credentials.get()API with the options - Client sends the assertion response to
POST /passkey/verify-authentication - Server validates the assertion, looks up the passkey, and creates a session
API Endpoints
The Passkey plugin exposes 7 endpoints. For full request/response details, see the OpenAPI Reference.
| Endpoint | Method | Description |
|---|---|---|
/passkey/generate-register-options | GET | Get WebAuthn creation options |
/passkey/verify-registration | POST | Verify and store a new passkey |
/passkey/generate-authenticate-options | GET | Get WebAuthn assertion options |
/passkey/verify-authentication | POST | Verify passkey and create session |
/passkey/list-user-passkeys | GET | List all passkeys for the user |
/passkey/delete-passkey | POST | Delete a passkey |
/passkey/update-passkey | POST | Update passkey name |
Client-Side Integration
Registration Example
// 1. Get registration options from the server
const optionsRes = await fetch('/auth/passkey/generate-register-options', {
headers: { 'Authorization': `Bearer ${sessionToken}` }
});
const options = await optionsRes.json();
// 2. Create credential using the browser API
const credential = await navigator.credentials.create({ publicKey: options });
// 3. Send the response back to the server
const verifyRes = await fetch('/auth/passkey/verify-registration', {
method: 'POST',
headers: {
'Authorization': `Bearer ${sessionToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
response: credential,
name: 'My Device'
})
});Authentication Example
// 1. Get authentication options
const optionsRes = await fetch('/auth/passkey/generate-authenticate-options', {
method: 'GET'
});
const options = await optionsRes.json();
// 2. Get assertion using the browser API
const assertion = await navigator.credentials.get({ publicKey: options });
// 3. Verify with the server
const verifyRes = await fetch('/auth/passkey/verify-authentication', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ response: assertion })
});
const { session } = await verifyRes.json();Errors
| Status | Condition |
|---|---|
| 400 | Invalid or missing WebAuthn response data |
| 400 | Invalid or expired challenge |
| 401 | Not authenticated (for registration/list/delete) |
| 403 | Passkey not found |