Better Auth in Rust

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

OptionTypeDefaultDescription
rp_idStringbase_url hostRelying Party ID (your domain)
rp_nameString"Better Auth"Relying Party display name
originStringrequest Origin headerExpected origin for WebAuthn responses
challenge_ttl_secsi64300 (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

  1. Client calls GET /passkey/generate-register-options to get WebAuthn creation options
  2. Client uses the browser's navigator.credentials.create() API with the options
  3. Client sends the credential response to POST /passkey/verify-registration
  4. Server validates the WebAuthn registration response and stores the passkey

Authentication Flow

  1. Client calls GET /passkey/generate-authenticate-options to get assertion options
  2. Client uses the browser's navigator.credentials.get() API with the options
  3. Client sends the assertion response to POST /passkey/verify-authentication
  4. 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.

EndpointMethodDescription
/passkey/generate-register-optionsGETGet WebAuthn creation options
/passkey/verify-registrationPOSTVerify and store a new passkey
/passkey/generate-authenticate-optionsGETGet WebAuthn assertion options
/passkey/verify-authenticationPOSTVerify passkey and create session
/passkey/list-user-passkeysGETList all passkeys for the user
/passkey/delete-passkeyPOSTDelete a passkey
/passkey/update-passkeyPOSTUpdate 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

StatusCondition
400Invalid or missing WebAuthn response data
400Invalid or expired challenge
401Not authenticated (for registration/list/delete)
403Passkey not found

On this page