Skip to content
v3.0.1 — MCP OAuth, organizations, passkeys, SSR adaptersChangelog
Convex Auth
Esc
↑↓navigate↵open⌘Jpreview
On this page

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 — returns totpURI and backupCodes.
  • 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.

Was this page helpful?