Two-Factor Authentication (TOTP + Backup Codes)
Use this guide when you already have a database-backed auth setup and want to add TOTP with backup codes.
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)
import { defineServerAuth } from '@nuxtjs/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
import { navigateTo } from '#imports'
import { defineClientAuth } from '@nuxtjs/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)
<script setup lang="ts">
definePageMeta({ auth: 'user' })
const client = useAuthClient()
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
Better Auth's admin plugin does not provide an API for disabling another user's two-factor authentication. The twoFactor.disable method applies to the current user and requires their password.
Use backup codes or a verified account-recovery process for users who lose every second factor. If your application adds an admin-assisted reset, implement it as an application-owned security flow with administrator re-authentication, an audit log, user notification, and session revocation. Do not call auth.api.admin.disableTwoFactor; that method does not exist.