This library is in early development. Expect breaking changes.
Guides

Two-Factor Authentication (TOTP + Backup Codes)

Enable two-factor auth using the Better Auth two-factor plugin.

Better Auth ships a twoFactor plugin with TOTP, OTP, and backup codes. It requires a database-backed setup (e.g., NuxtHub).

Enable the Plugin (Server)

server/auth.config.ts
import { defineServerAuth } from '@onmax/nuxt-better-auth/config'
import { twoFactor } from 'better-auth/plugins/two-factor'

export default defineServerAuth({
  emailAndPassword: { enabled: true },
  plugins: [
    twoFactor({
      // Optional: customize issuer shown in authenticator apps
      totpOptions: { issuer: 'My Nuxt App' },
    }),
  ],
})

Register Client Plugin

app/auth.config.ts
import { navigateTo } from '#imports'
import { defineClientAuth } from '@onmax/nuxt-better-auth/config'
import { twoFactorClient } from 'better-auth/client/plugins'

export default defineClientAuth({
  plugins: [
    twoFactorClient({
      onTwoFactorRedirect() {
        navigateTo('/two-factor')
      },
    }),
  ],
})

Enable 2FA for a User (TOTP)

pages/two-factor.vue
<script setup lang="ts">
definePageMeta({ auth: 'user' })
const { client } = useUserSession()
const password = ref('')
const qr = ref<string | null>(null)
const backupCodes = ref<string[] | null>(null)
const error = ref('')

async function enable2fa() {
  try {
    const res = await client?.twoFactor.enable({ password: password.value })
    if (res?.error) {
      error.value = res.error.message
      return
    }
    qr.value = res?.totpURI || null
    backupCodes.value = res?.backupCodes || null
  } catch (e) {
    error.value = 'Failed to enable 2FA. Please try again.'
  }
}
</script>

<template>
  <form @submit.prevent="enable2fa">
    <input v-model="password" type="password" placeholder="Password" />
    <button type="submit">Enable 2FA</button>
  </form>

  <div v-if="qr">
    <p>Scan this in your authenticator app:</p>
    <pre>{{ qr }}</pre>
  </div>

  <div v-if="backupCodes">
    <p>Backup codes (store securely):</p>
    <ul>
      <li v-for="code in backupCodes" :key="code">{{ code }}</li>
    </ul>
  </div>
</template>

Displaying the QR Code

Install a QR code library:

pnpm add qrcode

Render the QR code:

<script setup>
import QRCode from 'qrcode'

const qrImage = ref('')

watch(qr, async (uri) => {
  if (uri) {
    qrImage.value = await QRCode.toDataURL(uri)
  }
})
</script>

<template>
  <img v-if="qrImage" :src="qrImage" alt="Scan with authenticator app" />
</template>

Trust/Verify During Sign-In

When a user signs in and 2FA is required, twoFactorClient triggers onTwoFactorRedirect. On your /two-factor page, prompt for the code and call:

await client?.twoFactor.verifyTotp({
  code,
  trustDevice: true, // optional
})

Backup codes can be verified via:

await client?.twoFactor.verifyBackupCode({ code })

Disable 2FA

await client?.twoFactor.disable({ password })

Account Recovery

Backup Codes

Display backup codes to users during 2FA setup. Each code can only be used once.

<template>
  <div v-if="backupCodes.length">
    <p>Save these backup codes in a secure location:</p>
    <ul>
      <li v-for="code in backupCodes" :key="code">{{ code }}</li>
    </ul>
  </div>
</template>

Admin Recovery

For users who lose their device and backup codes, implement an admin recovery flow:

server/api/admin/reset-2fa.ts
export default defineEventHandler(async (event) => {
  await requireUserSession(event, { user: { role: 'admin' } })

  const { userId } = await readBody(event)
  const auth = serverAuth(event)

  // Reset 2FA for user (requires admin plugin)
  await auth.api.admin.disableTwoFactor({ userId })

  return { success: true }
})
See the Better Auth 2FA docs for provider options and OTP flows.