Tutorial paso a paso para añadir autenticación en dos pasos (TOTP) a una app Next.js usando el plugin de Better Auth.
Añadir 2FA (autenticación en dos pasos) a una app Next.js solía ser doloroso. En 2026, con Better Auth y su plugin de TOTP, son 30 minutos bien organizados.
Esta guía asume que ya tienes Better Auth funcionando. Si no, empieza por su quickstart y luego vuelve aquí.
bun add better-authYa lo tienes instalado, solo activamos el plugin.
src/lib/auth/server.ts:
import { betterAuth } from 'better-auth';
import { twoFactor } from 'better-auth/plugins';
import { prismaAdapter } from 'better-auth/adapters/prisma';
import { db } from '@/lib/db/client';
export const auth = betterAuth({
database: prismaAdapter(db, { provider: 'postgresql' }),
emailAndPassword: { enabled: true },
plugins: [
twoFactor({
issuer: 'CREA.MBA',
}),
],
});Better Auth necesita tablas adicionales para 2FA. En tu schema.prisma:
model TwoFactor {
id String @id
secret String
backupCodes String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}Después:
bunx prisma migrate dev --name add-2faCuando el usuario activa 2FA, generas un secreto TOTP y le devuelves un QR para que escanee.
src/app/api/auth/2fa/enable/route.ts:
import { auth } from '@/lib/auth/server';
import { headers } from 'next/headers';
import { NextResponse } from 'next/server';
export async function POST() {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const result = await auth.api.enableTwoFactor({
body: { password: '' }, // pedir contraseña en producción
headers: await headers(),
});
return NextResponse.json({
qrCode: result.totpURI,
backupCodes: result.backupCodes,
});
}'use client';
import { useState } from 'react';
import QRCode from 'react-qr-code';
export function Enable2FA() {
const [qrCode, setQrCode] = useState<string | null>(null);
const [backupCodes, setBackupCodes] = useState<string[]>([]);
const [verificationCode, setVerificationCode] = useState('');
async function startSetup() {
const res = await fetch('/api/auth/2fa/enable', { method: 'POST' });
const data = await res.json();
setQrCode(data.qrCode);
setBackupCodes(data.backupCodes);
}
async function verifyAndActivate() {
const res = await fetch('/api/auth/2fa/verify', {
method: 'POST',
body: JSON.stringify({ code: verificationCode }),
headers: { 'Content-Type': 'application/json' },
});
if (res.ok) alert('2FA activado');
}
if (!qrCode) {
return <button onClick={startSetup}>Activar 2FA</button>;
}
return (
<div>
<p>Escanea con tu app de autenticación:</p>
<QRCode value={qrCode} />
<p>Códigos de respaldo (guarda estos):</p>
<ul>
{backupCodes.map((c) => (
<li key={c}>
<code>{c}</code>
</li>
))}
</ul>
<input
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value)}
placeholder="Código de 6 dígitos"
/>
<button onClick={verifyAndActivate}>Verificar</button>
</div>
);
}En tu pantalla de login, después de validar email + password, comprueba si el usuario tiene 2FA activo. Si lo tiene, pídele el código TOTP antes de crear la sesión.
async function handleLogin(email: string, password: string) {
const res = await auth.signIn.email({ email, password });
if (res.requiresTwoFactor) {
setShow2FAInput(true);
return;
}
// login completo
}
async function handle2FACode(code: string) {
const res = await auth.twoFactor.verifyTotp({ code });
if (res.success) {
// login completo
}
}Si el usuario pierde el móvil, los backup codes salvan vidas. Better Auth los genera automáticamente. Cuando uno se usa, se invalida (no son reutilizables).
Tu UI debe permitir regenerarlos cuando el usuario quiera. Es buena práctica regenerarlos cada 6-12 meses.
1. El código no es válido: la hora del servidor y del móvil deben estar sincronizadas. Si tu servidor desfasa más de 30 segundos, los códigos TOTP fallan.
2. El QR no escanea: muchas apps prefieren otpauth://totp/... en lugar de la URL pelada.
Better Auth ya lo da en formato correcto.
3. Backup codes en texto plano: nunca guardes los backup codes en plano. Better Auth los hashea por defecto.
30 minutos para 2FA bien hecho. La parte más larga suele ser la UI del setup; la lógica del plugin Better Auth es prácticamente plug-and-play.
Hoy no hay excusa para no tenerlo. Y a tus usuarios B2B les va a parar el oído.
Suscríbete para más tutoriales y tips sobre crear productos con IA
La integración real de Tailwind v4 + shadcn 2.3.0 en Next.js 16 App Router. OKLCH, dark mode sin FOUC, Geist con next/font y el bug silencioso de hsl(var()) que rompe tus estilos sin avisar.