> ## Documentation Index
> Fetch the complete documentation index at: https://docs.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](https://docs.dynamic.xyz/docs/javascript/reference/client/create-dynamic-client.md), [Initializing the Dynamic client](https://docs.dynamic.xyz/docs/javascript/reference/client/initialize-dynamic-client.md)), and have a signed-in user (see [Email & SMS OTP sign-in](https://docs.dynamic.xyz/docs/javascript/building-ui/email-sms-otp-sign-in.md) or [Social OAuth](https://docs.dynamic.xyz/docs/javascript/building-ui/social-oauth-redirect-handling.md)).

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

> [tab] 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],
>   });
> }
> ```

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

> [tab] 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] 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>
>   );
> }
> ```

## Showing recovery codes

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

> [tab] 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] 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>;
> }
> ```

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

> [tab] 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] 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>
>   );
> }
> ```

## 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](https://docs.dynamic.xyz/docs/javascript/building-ui/step-up-authentication.md) — require MFA before a sensitive action
* [Post-login user setup](https://docs.dynamic.xyz/docs/javascript/building-ui/post-auth-user-setup.md) — prompt for MFA enrollment when it's required
* [Email & SMS OTP sign-in](https://docs.dynamic.xyz/docs/javascript/building-ui/email-sms-otp-sign-in.md) — the sign-in step before MFA


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.