Two-factor authentication
TOTP and backup codes with convex-auth.
convex-auth implements TOTP-based 2FA with backup codes. The actions are re-exported from convex/auth.ts and wrapped by the React client.
Enabling 2FA
Use useConvexAuthClient to start enrollment. The user must re-authenticate with their password.
import { useConvexAuthClient } from "@vortex-api/convex-auth/react";
import QRCode from "qrcode.react";
export function EnableTwoFactor() {
const client = useConvexAuthClient();
async function start(password: string) {
const { data, error } = await client.twoFactor.enable({ password });
if (error) {
console.error(error);
return;
}
// data.totpURI is an otpauth:// URL. Render it as a QR code.
// data.backupCodes are one-time recovery codes. Show them once.
return data;
}
async function confirm(code: string) {
const { error } = await client.twoFactor.verifyTotp({ code });
if (error) console.error(error);
}
// ...
}
twoFactor.enable returns totpURI, backupCodes, and error. 2FA is not active until the user confirms a TOTP code via twoFactor.verifyTotp.
Signing in with 2FA
After 2FA is enabled, signIn returns a twoFactorChallengeToken instead of a session token. The client must call twoFactor.verifyTotp or twoFactor.verifyBackupCode:
const client = useConvexAuthClient();
async function signIn(email: string, password: string) {
const { data, error } = await client.signIn.email({ email, password });
if (error) {
// handle error
}
// The client stores the twoFactorChallengeToken internally for the verify step.
}
async function submit2fa(code: string) {
const { error } = await client.twoFactor.verifyTotp({ code, trustDevice: true });
if (error) {
// try backup code
await client.twoFactor.verifyBackupCode({ code, trustDevice: true });
}
}
trustDevice skips 2FA on the current device for the configured trust window.
The pending challenge token is opaque, single-use, and expires after ten minutes — it is not a session and cannot be used as one.
Passkeys
A passkey ceremony that verifies the user (userVerification: "required", the default) already proves possession plus verification, so passkey sign-in satisfies 2FA on its own. If you relax passkey.userVerification to "preferred" or "discouraged" and the account has 2FA enabled, passkey signIn resolves to the same twoFactorRedirect challenge as password sign-in — complete it with twoFactor.verifyTotp or verifyBackupCode. Revoking the passkey also consumes its in-flight pending challenges.
Disabling 2FA
const client = useConvexAuthClient();
await client.twoFactor.disable({ password });
Regenerating backup codes
const client = useConvexAuthClient();
const { data } = await client.twoFactor.generateBackupCodes();
// data.backupCodes — show once
Server actions
If you need direct access from a Convex component or mutation, the underlying actions are available in convex/auth.ts:
twoFactorEnable— returnstotpURIandbackupCodes.twoFactorVerifyTOTP— verifies a TOTP code during sign-in.twoFactorVerifyBackupCode— verifies a backup code.twoFactorDisable— disables 2FA for the current user.twoFactorGenerateBackupCodes— generates new backup codes.
These are all exported from the auth object returned by convexAuth.