Passkeys and two-factor
Add WebAuthn passkeys and TOTP two-factor authentication, including the admin panel views.
Both plugins are supported first-class: the plugin generates their collections and wires their admin UI.
Passkeys
Install and enable
npm install @better-auth/passkeyimport { passkey } from '@better-auth/passkey'
export const betterAuthOptions = {
plugins: [
passkey({
rpID: 'localhost',
rpName: 'My App',
origin: 'http://localhost:3000',
}),
],
} satisfies BetterAuthOptionsrpID is the registrable domain without protocol or port (localhost in development,
example.com in production). origin is the full origin. Both must match what the browser
sees or WebAuthn refuses to run.
Add the client plugin
import { passkeyClient } from '@better-auth/passkey/client'
export const authClient = createAuthClient({
plugins: [passkeyClient()],
})Regenerate the import map
pnpm payload generate:importmapThe passkey button appears on the admin login view automatically, because setLoginMethods
adds passkey when the plugin is detected.
This creates a passkeys collection with the credential ID, public key, counter and a
user relationship.
Managing passkeys
A Passkeys server component is exported for building a management screen:
import { Passkeys } from 'payload-auth/better-auth/plugin/rsc'Or drive it from the client:
await authClient.passkey.addPasskey({ name: 'MacBook' })
const { data } = await authClient.passkey.listUserPasskeys()
await authClient.passkey.deletePasskey({ id })Two-factor authentication
Enable the plugin
import { twoFactor } from 'better-auth/plugins'
export const betterAuthOptions = {
plugins: [
twoFactor({
issuer: 'My App',
otpOptions: {
async sendOTP({ user, otp }) {
await sendEmail({ to: user.email, subject: 'Your code', text: otp })
},
},
}),
],
} satisfies BetterAuthOptionsissuer is the label shown in authenticator apps.
Add the client plugin
import { twoFactorClient } from 'better-auth/client/plugins'
export const authClient = createAuthClient({
plugins: [
twoFactorClient({
onTwoFactorRedirect() {
window.location.href = '/two-factor'
},
}),
],
})Regenerate the import map
pnpm payload generate:importmapThis creates a twoFactors collection (secret, backup codes, user relationship), adds a
twoFactorEnabled checkbox to users, and registers the /admin/two-factor-verify view.
The admin challenge
When an admin with 2FA enabled signs in, they are redirected to
/admin/two-factor-verify, which accepts a TOTP code or a backup code. This view is only
registered when the twoFactor plugin is present.
Enrolling
const { data } = await authClient.twoFactor.enable({ password })
// data.totpURI → render as a QR code
// data.backupCodes → show once, tell the user to store them
await authClient.twoFactor.verifyTotp({ code })A TwoFactorAuth client component that handles enrolment (QR code, verification, backup
codes) is exported for reuse:
import { TwoFactorAuth } from 'payload-auth/better-auth/plugin/client'Locking yourself out
If a user enables 2FA and loses both their authenticator and their backup codes, they
cannot sign in. As an admin you can clear the situation by deleting their twoFactors
record and unchecking twoFactorEnabled on their user document in the admin panel.
Other passwordless methods
magicLink, emailOTP, phoneNumber and anonymous are all supported. Enable the Better
Auth plugin, add its client counterpart, and — if you want it on the admin login screen —
list the matching method in admin.loginMethods (magicLink, emailOTP, phoneOTP,
phoneMagicLink, phonePassword).