Skip to main content
The React SDK offers three approaches to step-up authentication, from fully managed to fully custom:
  1. promptStepUpAuth — One call that automatically picks the right verification method and shows Dynamic’s built-in UI. Recommended for most use cases.
  2. promptMfa / promptReauthenticate — Dynamic’s built-in UI with explicit control over which verification path is used.
  3. Individual verify methods — Full headless control for building your own UI.
After verification, the elevated access token is automatically stored and applied to subsequent API calls. You never need to manually handle the token. For concepts, scopes, and token lifecycle, see Step-up authentication overview.

Prerequisites

  • DynamicContextProvider configured with your environment ID
  • At least one verification method enabled in your dashboard security settings
  • Step-up authentication enabled for your environment
promptStepUpAuth checks whether the user has MFA methods and automatically routes to the correct UI — passkey/TOTP for MFA users, OTP/wallet for non-MFA users.
import { useStepUpAuthentication } from '@dynamic-labs/sdk-react-core';
import { TokenScope } from '@dynamic-labs/sdk-api-core';

const ExportButton = () => {
  const { isStepUpRequired, promptStepUpAuth } = useStepUpAuthentication();

  const handleExport = async () => {
    if (await isStepUpRequired({ scope: TokenScope.Walletexport })) {
      try {
        await promptStepUpAuth({
          requestedScopes: [TokenScope.Walletexport],
        });
      } catch {
        return; // User cancelled or verification failed
      }
    }

    // Token is stored — proceed with the operation.
    // The SDK attaches it to the API call automatically.
    await exportWallet();
  };

  return <button onClick={handleExport}>Export Wallet</button>;
};

How promptStepUpAuth routes

User stateVerification methodUI shown
Has passkey + TOTPMost recently created MFA methodPasskey prompt or TOTP input
Has passkey onlyPasskeyBrowser passkey prompt
Has TOTP onlyTOTPDynamic’s TOTP code input
No MFA, has emailEmail OTPDynamic’s re-auth method selection
No MFA, has SMSSMS OTPDynamic’s re-auth method selection
No MFA, has walletWallet signatureDynamic’s re-auth method selection
No MFA, has socialSocial OAuthDynamic’s re-auth method selection
Multiple non-MFA methodsUser selectsDynamic’s method chooser

Approach 2: Dynamic’s built-in UI (explicit path)

Use promptMfa or promptReauthenticate when you want to control which verification path is used while still using Dynamic’s UI:
Shows Dynamic’s MFA verification UI (passkey or TOTP). Only works when the user has registered MFA methods.
const { promptMfa } = useStepUpAuthentication();

await promptMfa({ requestedScopes: [TokenScope.Walletexport] });

Approach 3: Headless (custom UI)

For full control over the UI, use the individual verify methods. You build the UI — the hook handles verification and token storage.

Checking if step-up is required

checkStepUpAuth is the recommended approach. It performs a server-authoritative check and returns both whether step-up is required and the available credentials, so you can route the user to the right verification method without extra API calls.
const { checkStepUpAuth } = useStepUpAuthentication();

const { isRequired, credentials, defaultCredentialId } =
  await checkStepUpAuth({ scope: TokenScope.Walletexport });

if (isRequired) {
  // credentials contains the available verification methods
  // Each credential has: { id, format, type?, alias? }
  //
  // defaultCredentialId is the recommended credential to authenticate with:
  // - For MFA users: the configured default device or most recently added
  // - For re-auth users: the credential used to sign in
  const defaultCred = credentials.find((c) => c.id === defaultCredentialId);
}
It does a fast local check first (if an elevated token exists, returns { isRequired: false } without an API call), then calls the backend. On failure, it defaults to { isRequired: true } for safety.

isStepUpRequired (simple boolean)

Use isStepUpRequired if you only need a boolean and don’t need the available credentials:
const { isStepUpRequired } = useStepUpAuthentication();

if (await isStepUpRequired({ scope: TokenScope.Walletexport })) {
  // Show your step-up verification UI
}
Returns true when step-up auth is enabled for the environment and no valid elevated token exists for the scope. Returns false if the token already exists or step-up is not enabled.

Email / SMS OTP

const { sendOtp, verifyOtp } = useStepUpAuthentication();

// Send OTP to the user's sign-in enabled email or SMS credential
const verification = await sendOtp();
// verification.verificationType tells you whether it was 'email' or 'sms'

// After the user enters the code:
await verifyOtp({
  verificationToken: code,
  requestedScopes: [TokenScope.Credentiallink],
});
To target a specific credential, pass credentialId at call time or when initializing the hook:
// Option 1: pass credentialId at call time (preferred for dynamic selection)
const { sendOtp, verifyOtp } = useStepUpAuthentication();
await sendOtp({ credentialId: 'specific-email-credential-id' });

// Option 2: pass credentialId when initializing the hook
const { sendOtp, verifyOtp } = useStepUpAuthentication({
  credentialId: 'specific-email-credential-id',
});

Wallet signature (external wallets only)

Wallet-based step-up verification is only available for external wallets. Embedded wallets cannot be used for step-up authentication.
const { verifyWallet } = useStepUpAuthentication();

// Pass walletId at call time — defaults to the first connected wallet when omitted
await verifyWallet({
  walletId: walletCredentialId,
  requestedScopes: [TokenScope.Walletexport],
});

Passkey MFA

const { verifyPasskeyMfa } = useStepUpAuthentication();

// Triggers the browser's passkey prompt
await verifyPasskeyMfa({ requestedScopes: [TokenScope.Walletexport] });

TOTP MFA

const { verifyTotpMfa } = useStepUpAuthentication();

await verifyTotpMfa({
  code: totpCode,
  requestedScopes: [TokenScope.Walletexport],
});

Social (OAuth)

import { ProviderEnum } from '@dynamic-labs/sdk-api-core';

const { verifySocial } = useStepUpAuthentication();

// Opens a popup for the user to re-authenticate with their linked social account
await verifySocial({
  provider: ProviderEnum.Google,
  requestedScopes: [TokenScope.Credentiallink],
});
The user must have the social account already linked. The SDK opens a popup to the OAuth provider — no redirect is needed. If the browser blocks the popup (e.g., on mobile), the SDK falls back to redirect and resumes the flow automatically on return.

Recovery code

const { verifyRecoveryCode } = useStepUpAuthentication();

await verifyRecoveryCode({
  code: recoveryCode,
  requestedScopes: [TokenScope.Credentialunlink],
});

Error handling

All methods update the shared state object and throw on failure:
const { verifyPasskeyMfa, state, resetState } = useStepUpAuthentication();

try {
  await verifyPasskeyMfa({ requestedScopes: [TokenScope.Walletexport] });
} catch (error) {
  // state.error contains the error message
  // Call resetState() before retrying
}
// Render loading and error states
{state.isLoading && <Spinner />}
{state.error && (
  <div>
    <p>{state.error}</p>
    <button onClick={resetState}>Try again</button>
  </div>
)}

Full example: headless credential linking

import { FC, useState } from 'react';
import { useStepUpAuthentication } from '@dynamic-labs/sdk-react-core';
import { TokenScope } from '@dynamic-labs/sdk-api-core';

const LinkEmailButton: FC = () => {
  const { isStepUpRequired, sendOtp, verifyOtp, state, resetState } =
    useStepUpAuthentication();
  const [step, setStep] = useState<'idle' | 'otp'>('idle');
  const [code, setCode] = useState('');

  const handleStart = async () => {
    resetState();

    if (!(await isStepUpRequired({ scope: TokenScope.Credentiallink }))) {
      // Step-up not needed — proceed directly to link
      await linkEmail();
      return;
    }

    await sendOtp();
    setStep('otp');
  };

  const handleVerify = async () => {
    await verifyOtp({
      verificationToken: code,
      requestedScopes: [TokenScope.Credentiallink],
    });
    // Token stored — proceed with linking
    await linkEmail();
    setStep('idle');
  };

  if (step === 'otp') {
    return (
      <div>
        <input
          value={code}
          onChange={(e) => setCode(e.target.value)}
          placeholder="Enter verification code"
        />
        <button onClick={handleVerify} disabled={state.isLoading}>
          {state.isLoading ? 'Verifying...' : 'Verify'}
        </button>
        {state.error && <p>{state.error}</p>}
      </div>
    );
  }

  return <button onClick={handleStart}>Link Email</button>;
};

Hook reference

See the full useStepUpAuthentication reference for all return values and parameter types.

External auth (Bring Your Own Auth)

If you use external auth (BYOA), your backend can issue elevated access tokens directly using the useExternalAuth hook — no user interaction required. See the External Auth Step-Up guide.