Passkey
WebAuthn/FIDO2 biometric and hardware key authentication.
Passkeys use the WebAuthn standard (FIDO2) to authenticate users with device biometrics (Touch ID, Face ID, Windows Hello) or hardware security keys. No password is ever created or stored.
Setup
Install
pnpm add kavachosAdd the plugin
import { createKavach } from 'kavachos';
import { passkey } from 'kavachos/plugins/passkey';
const kavach = await createKavach({
database: { provider: 'postgres', url: process.env.DATABASE_URL! },
secret: process.env.KAVACH_SECRET!,
baseUrl: 'https://auth.example.com',
plugins: [
passkey({
rpName: 'My App', // Shown in the browser prompt
rpId: 'example.com', // Must match your domain, no protocol
}),
],
});rpId must be a registrable domain suffix of the origin. For https://app.example.com, valid values are app.example.com or example.com. Localhost works during development.
Registration ceremony
A passkey is tied to a specific device. Users register once per device they want to use.
Get registration options
POST /auth/passkey/register/options
Returns a WebAuthn challenge from the server. Requires an active session (the user must already be signed in to register a passkey).
const res = await fetch('/auth/passkey/register/options', {
method: 'POST',
credentials: 'include',
});
const options = await res.json();Create the credential
Pass the options to the browser's WebAuthn API:
import { startRegistration } from '@simplewebauthn/browser';
const credential = await startRegistration(options);Verify and store
POST /auth/passkey/register/verify
await fetch('/auth/passkey/register/verify', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credential),
});On success, the credential is stored and the passkey is active.
Authentication ceremony
Get authentication options
POST /auth/passkey/authenticate/options
Does not require a session — this is the start of sign-in.
const res = await fetch('/auth/passkey/authenticate/options', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'user@example.com' }), // optional
});
const options = await res.json();Omitting email returns options for any registered passkey on the device (useful for conditional UI).
Get the assertion
import { startAuthentication } from '@simplewebauthn/browser';
const assertion = await startAuthentication(options);Verify and start session
POST /auth/passkey/authenticate/verify
const res = await fetch('/auth/passkey/authenticate/verify', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(assertion),
});
const { userId, sessionId } = await res.json(); On success, a session cookie is set.
Managing credentials
Users can register multiple passkeys across different devices.
List credentials
GET /auth/passkey/credentials
const res = await fetch('/auth/passkey/credentials', {
credentials: 'include',
});
const { credentials } = await res.json();
// [{ id, name, createdAt, lastUsedAt, deviceType }]Delete a credential
DELETE /auth/passkey/credentials/:id
await fetch(`/auth/passkey/credentials/${credentialId}`, {
method: 'DELETE',
credentials: 'include',
});If a user deletes their last passkey and has no other sign-in method, they will be locked out. Check the credential count before allowing deletion, or prompt the user to set a password first.
Endpoints
| Endpoint | Auth required | Description |
|---|---|---|
POST /auth/passkey/register/options | Yes | Get challenge to start registration |
POST /auth/passkey/register/verify | Yes | Store the new credential |
POST /auth/passkey/authenticate/options | No | Get challenge to start sign-in |
POST /auth/passkey/authenticate/verify | No | Verify assertion, set session |
GET /auth/passkey/credentials | Yes | List registered credentials |
DELETE /auth/passkey/credentials/:id | Yes | Remove a credential |
Options
| Option | Type | Default | Description |
|---|---|---|---|
rpName | string | required | App name shown in the browser passkey prompt |
rpId | string | required | Relying party ID, must match your domain |
timeout | number | 60000 | WebAuthn operation timeout in milliseconds |
attestation | string | 'none' | Attestation preference: 'none', 'indirect', 'direct' |
userVerification | string | 'preferred' | Whether biometric check is required: 'required', 'preferred', 'discouraged' |