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. This is enforced by webauthn-rs, so combinations like rp_id = "example.com" with origin = "https://other.test" are rejected during configuration.
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 |