> ## Documentation Index
> Fetch the complete documentation index at: https://www.dynamic.xyz/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# MFA enrollment & management

> Enroll an authenticator app (TOTP), verify it, show recovery codes, and let users manage their MFA devices.

## Prerequisites

Before this page: create and initialize a Dynamic client (see [Creating a Dynamic client](/docs/javascript/reference/client/create-dynamic-client), [Initializing the Dynamic client](/docs/javascript/reference/client/initialize-dynamic-client)), and have a signed-in user (see [Email & SMS OTP sign-in](/docs/javascript/building-ui/email-sms-otp-sign-in) or [Social OAuth](/docs/javascript/building-ui/social-oauth-redirect-handling)).

## What you'll build

* **Enroll** an authenticator app (TOTP) and verify it with a code.
* **Show recovery codes** so users can get back in if they lose the device.
* **Manage devices** — list them, set a default, and remove one.

## Before you start: elevated access tokens

`registerTotpMfaDevice` and `deleteMfaDevice` (below) both require an elevated access token — `TokenScope.Credentiallink` and `TokenScope.Credentialunlink` respectively — with no fallback. Calling either directly returns a 403 (`Elevated access token required`) unless the user already holds one.

Check first with `checkStepUpAuth({ scope })`. If it's required, re-verify the user with an existing credential (email OTP shown here) and pass the scope in `requestedScopes` — the resulting elevated token is picked up automatically by the gated call that follows.

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={"system"}
    import {
      checkStepUpAuth,
      sendEmailOTP,
      verifyOTP,
      TokenScope,
    } from '@dynamic-labs-sdk/client';

    async function ensureElevatedAccess(scope: TokenScope) {
      const { isRequired } = await checkStepUpAuth({ scope });
      if (!isRequired) return;

      // Re-verify with an existing credential — email OTP shown here.
      const otpVerification = await sendEmailOTP({ email });
      const code = await promptForCode(); // your implementation
      await verifyOTP({
        otpVerification,
        verificationToken: code,
        requestedScopes: [scope],
      });
    }
    ```
  </Tab>
</Tabs>

## Enrolling a TOTP device

`registerTotpMfaDevice` returns `{ id, secret, uri }`. Render `uri` as a QR code (with your preferred QR library) and show `secret` as a manual-entry fallback. The user scans it, then enters the 6-digit code so you can verify with `authenticateTotpMfaDevice`.

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={"system"}
    import {
      registerTotpMfaDevice,
      authenticateTotpMfaDevice,
      TokenScope,
    } from '@dynamic-labs-sdk/client';

    async function enroll() {
      await ensureElevatedAccess(TokenScope.Credentiallink);

      const { id, secret, uri } = await registerTotpMfaDevice();

      // renderQrCode / showSecret are your implementation.
      renderQrCode(uri);
      showSecret(secret);

      // promptForCode is your implementation — the 6-digit code from the app.
      const code = await promptForCode();

      try {
        await authenticateTotpMfaDevice({ code, deviceId: id });
        // showEnrolled is your implementation.
        showEnrolled();
      } catch (error) {
        // handleMfaError is your implementation — see the error table below.
        handleMfaError(error);
      }
    }
    ```
  </Tab>

  <Tab title="React">
    ```tsx theme={"system"}
    import { useState } from 'react';
    import { TokenScope } from '@dynamic-labs-sdk/client';
    import {
      useRegisterTotpMfaDevice,
      useAuthenticateTotpMfaDevice,
    } from '@dynamic-labs-sdk/react-hooks';

    function EnrollTotp() {
      const { mutateAsync: register } = useRegisterTotpMfaDevice();
      const { mutateAsync: verify, isPending } = useAuthenticateTotpMfaDevice();
      const [device, setDevice] = useState<{ id: string; uri: string } | null>(null);

      const startEnrollment = async () => {
        // ensureElevatedAccess is the helper defined above.
        await ensureElevatedAccess(TokenScope.Credentiallink);
        setDevice(await register());
      };

      if (!device) {
        return (
          <button type="button" onClick={startEnrollment}>
            Set up authenticator app
          </button>
        );
      }

      return (
        <form
          onSubmit={async (event) => {
            event.preventDefault();
            const code = String(new FormData(event.currentTarget).get('code'));
            await verify({ code, deviceId: device.id });
          }}
        >
          {/* Render device.uri as a QR code with your QR library. */}
          <input name="code" inputMode="numeric" autoComplete="one-time-code" />
          <button type="submit" disabled={isPending}>Verify</button>
        </form>
      );
    }
    ```
  </Tab>
</Tabs>

## Showing recovery codes

After enrolling, show the user their recovery codes once and record that they saw them with `acknowledgeRecoveryCodes`.

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={"system"}
    import {
      getMfaRecoveryCodes,
      acknowledgeRecoveryCodes,
    } from '@dynamic-labs-sdk/client';

    async function showRecoveryCodes() {
      const { recoveryCodes } = await getMfaRecoveryCodes();
      // displayCodes is your implementation — show them once, let the user save them.
      await displayCodes(recoveryCodes);
      await acknowledgeRecoveryCodes();
    }
    ```
  </Tab>

  <Tab title="React">
    ```tsx theme={"system"}
    import { getMfaRecoveryCodes } from '@dynamic-labs-sdk/client';
    import { useAcknowledgeRecoveryCodes } from '@dynamic-labs-sdk/react-hooks';

    function RecoveryCodes() {
      const { mutateAsync: acknowledge } = useAcknowledgeRecoveryCodes();

      const onReveal = async () => {
        const { recoveryCodes } = await getMfaRecoveryCodes();
        // displayCodes is your implementation.
        await displayCodes(recoveryCodes);
        await acknowledge();
      };

      return <button type="button" onClick={onReveal}>Show recovery codes</button>;
    }
    ```
  </Tab>
</Tabs>

## Managing devices

List enrolled devices, set a default, and remove one.

<Warning>
  Removal needs the `Credentialunlink` elevated access token from [Before you start: elevated access tokens](#before-you-start-elevated-access-tokens). A plain `authenticateTotpMfaDevice({ code, deviceId })` re-verify — with no `requestedScopes` — does **not** produce a token `deleteMfaDevice` will accept, even though its `mfaAuthToken` parameter is optional. Pass `requestedScopes: [TokenScope.Credentialunlink]` on the re-verification call instead; `deleteMfaDevice` then picks up the resulting elevated token automatically.
</Warning>

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={"system"}
    import {
      getMfaDevices,
      setDefaultMfaDevice,
      authenticateTotpMfaDevice,
      deleteMfaDevice,
      TokenScope,
    } from '@dynamic-labs-sdk/client';

    async function listDevices(list: HTMLUListElement) {
      const devices = await getMfaDevices();
      list.replaceChildren();
      for (const device of devices) {
        const item = document.createElement('li');
        item.textContent = `${device.type}${device._default ? ' (default)' : ''}`;
        list.append(item);
      }
    }

    async function makeDefault(deviceId: string) {
      await setDefaultMfaDevice({ deviceId });
    }

    async function remove(deviceId: string, code: string) {
      // Re-verifying with the Credentialunlink scope grants the elevated
      // token deleteMfaDevice() needs — it's picked up automatically.
      await authenticateTotpMfaDevice({
        code,
        deviceId,
        requestedScopes: [TokenScope.Credentialunlink],
      });
      await deleteMfaDevice({ deviceId });
    }
    ```
  </Tab>

  <Tab title="React">
    ```tsx theme={"system"}
    import { TokenScope } from '@dynamic-labs-sdk/client';
    import {
      useGetMfaDevices,
      useAuthenticateTotpMfaDevice,
      useDeleteMfaDevice,
    } from '@dynamic-labs-sdk/react-hooks';

    function MfaDevices() {
      const { data: devices } = useGetMfaDevices();
      const { mutateAsync: authenticate } = useAuthenticateTotpMfaDevice();
      const { mutateAsync: remove } = useDeleteMfaDevice();

      const onRemove = async (deviceId: string, code: string) => {
        await authenticate({
          code,
          deviceId,
          requestedScopes: [TokenScope.Credentialunlink],
        });
        await remove({ deviceId });
      };

      return (
        <ul>
          {devices?.map((device) => (
            <li key={device.id}>
              {device.type}
              {device._default ? ' (default)' : null}
              <button
                type="button"
                onClick={() => {
                  // promptForCode is your implementation — the 6-digit code from
                  // the device being removed, or another registered device.
                  void promptForCode().then((code) => onRemove(device.id!, code));
                }}
              >
                Remove
              </button>
            </li>
          ))}
        </ul>
      );
    }
    ```
  </Tab>
</Tabs>

## Handling errors

| Error                                          | Cause                                                       | What to do                                                                                                                                                                   |
| ---------------------------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MfaInvalidOtpError`                           | The entered code is wrong or expired                        | Ask the user to re-enter the current code from their app.                                                                                                                    |
| `MfaRateLimitedError`                          | Too many attempts                                           | Ask the user to wait before trying again.                                                                                                                                    |
| `403 Elevated access token required` on enroll | Re-verification didn't request the `Credentiallink` scope   | Run `ensureElevatedAccess(TokenScope.Credentiallink)` before `registerTotpMfaDevice` — see [Before you start](#before-you-start-elevated-access-tokens).                     |
| `403 Elevated access token required` on delete | Re-verification didn't request the `Credentialunlink` scope | Pass `requestedScopes: [TokenScope.Credentialunlink]` on the `authenticateTotpMfaDevice` call before retrying `deleteMfaDevice` — see [Managing devices](#managing-devices). |

## See also

* [Step-up authentication](/docs/javascript/building-ui/step-up-authentication) — require MFA before a sensitive action
* [Post-login user setup](/docs/javascript/building-ui/post-auth-user-setup) — prompt for MFA enrollment when it's required
* [Email & SMS OTP sign-in](/docs/javascript/building-ui/email-sms-otp-sign-in) — the sign-in step before MFA
