auth pattern
Verify 2FA
TOTP / backup-code entry after sign-in for users with 2FA enabled.
When to use it
Sign-in returned twoFactorRedirect. User must complete second factor before session commits.
- Verify code in under 10s
- Backup-code escape hatch
Layout regions
- header
- card
- footer
States
- default
- loading
- error
- partial
- success
Example
Next.js — TOTP with backup-code fallback, auto-submit, lockout countdown
'use client'
import { useState } from 'react'
import { authClient } from '@/lib/auth-client'
import { useRouter, useSearchParams } from 'next/navigation'
import { toast } from 'sonner'
export default function Verify2FA() {
const router = useRouter()
const next = useSearchParams().get('next') ?? '/dashboard'
const [mode, setMode] = useState<'totp' | 'backup'>('totp')
const [submitting, setSubmitting] = useState(false)
const verify = async (code: string) => {
setSubmitting(true)
try {
const r = mode === 'totp'
? await authClient.twoFactor.verifyTotp({ code })
: await authClient.twoFactor.verifyBackupCode({ code })
if (r.error) { toast.error(r.error.message ?? 'Wrong code'); return false }
router.replace(next); return true
} finally { setSubmitting(false) }
}
return (
<Card className="mx-auto max-w-md">
<CardHeader>
<CardTitle>Two-factor verification</CardTitle>
<CardDescription>{mode === 'totp' ? 'Enter the 6-digit code from your authenticator app.' : 'Enter one of your backup codes.'}</CardDescription>
</CardHeader>
<CardContent>
{mode === 'totp'
? <OTPInput length={6} autoComplete="one-time-code" disabled={submitting} onComplete={verify} />
: <BackupCodeInput onSubmit={verify} disabled={submitting} />}
</CardContent>
<CardFooter>
<button type="button" onClick={() => setMode((m) => m === 'totp' ? 'backup' : 'totp')} className="text-sm">
{mode === 'totp' ? 'Use a backup code instead' : 'Back to authenticator'}
</button>
</CardFooter>
</Card>
)
}The full Verify 2FA pattern specifies 4 exact microcopy strings, 1 motion spec, 4 composition rules, 2 more code examples — all gated behind the full recipe. Get the full recipe.