---
title: Two-factor authentication
description: TOTP and backup codes with convex-auth.
sidebar:
  order: 7
---

`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.

```tsx
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`:

```tsx
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

```tsx
const client = useConvexAuthClient();
await client.twoFactor.disable({ password });
```

## Regenerating backup codes

```tsx
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`.
