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

# Step-Up Authentication

> Headless step-up authentication using the JavaScript SDK — check, verify, and the SDK handles the rest.

The JavaScript SDK gives you full control over step-up authentication without any UI framework. Works with vanilla JS, React, Vue, Svelte, Angular, or any other setup.

For concepts, scopes, and token lifecycle, see [Step-up authentication overview](https://docs.dynamic.xyz/docs/auth/step-up-auth.md).

## Prerequisites

* `@dynamic-labs-sdk/client` initialized
* At least one verification method enabled in your [console security settings](https://console.dynamic.xyz/dashboard/security)
* Step-up authentication enabled for your environment

## Quick start

The pattern is always: **check → verify → proceed**. After verification, the elevated access token is automatically stored and applied to subsequent Dynamic API calls. You never need to manually handle the token.

```javascript theme={"system"}
import { checkStepUpAuth, authenticatePasskeyMFA, TokenScope } from '@dynamic-labs-sdk/client';

const exportPrivateKey = async () => {
  // 1. Check if step-up is required and get available credentials
  const { isRequired, credentials } = await checkStepUpAuth({
    scope: TokenScope.Walletexport,
  });

  if (isRequired) {
    // 2. Verify — the token is stored and applied automatically
    await authenticatePasskeyMFA({
      requestedScopes: [TokenScope.Walletexport],
    });
  }

  // 3. Proceed — the SDK attaches the token to the relevant API call
  await performExport();
};
```

## Checking step-up requirements (recommended)

`checkStepUpAuth` is the recommended way to determine whether step-up is required. It performs a fast local check first (if a valid elevated token already exists, it returns immediately without an API call), then falls back to a server-authoritative check that evaluates your environment configuration, SDK version, MFA settings, and action-based MFA requirements. On failure, it defaults to `{ isRequired: true }` for safety.

```javascript theme={"system"}
import { checkStepUpAuth, TokenScope } from '@dynamic-labs-sdk/client';

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

if (isRequired) {
  // credentials contains the user's 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);
  console.log('Recommended credential:', defaultCred);
}
```

The response type:

```typescript theme={"system"}
import { JwtVerifiedCredentialFormatEnum } from '@dynamic-labs-sdk/client';
type StepUpCheckResponse = {
  isRequired: boolean;
  credentials: StepUpCredential[];
  /** The recommended credential to authenticate with.
   *  For MFA: the user's default device or most recently added.
   *  For re-auth: the credential used to sign in. */
  defaultCredentialId?: string;
};

type StepUpCredential = {
  id: string;
  format: JwtVerifiedCredentialFormatEnum;
  type?: string;
  alias?: string;
};
```

## Checking for a valid token (alternative)

> [!NOTE]
>
> `checkStepUpAuth` (shown above) is the recommended approach. It combines the token check with credential discovery in a single call. Use `getElevatedAccessToken` only if you need a simple synchronous local-only check.

Use `getElevatedAccessToken` to check if the user already has a non-expired elevated token for a scope. Pass `consume: false` to check without removing a single-use token:

```javascript theme={"system"}
import { getElevatedAccessToken, TokenScope } from '@dynamic-labs-sdk/client';

if (getElevatedAccessToken({ scope: TokenScope.Walletexport, consume: false })) {
  // Token exists and is valid — proceed with the operation
} else {
  // No valid token — trigger verification
}
```

## Verification methods

Pass `requestedScopes` to any verification method. After success, the elevated token is automatically stored and applied to Dynamic API calls.

> [tab] Email OTP
>
> ```javascript theme={"system"}
> import { sendEmailOTP, verifyOTP, TokenScope } from '@dynamic-labs-sdk/client';
>
> // 1. Send the OTP to the user's verified email
> const otpVerification = await sendEmailOTP({ email });
>
> // 2. Verify with the code the user entered
> await verifyOTP({
>   otpVerification,
>   verificationToken: otpCode,
>   requestedScopes: [TokenScope.Credentiallink],
> });
> // Token is now stored and will be applied automatically
> ```

> [tab] SMS OTP
>
> ```javascript theme={"system"}
> import { sendSmsOTP, verifyOTP, TokenScope } from '@dynamic-labs-sdk/client';
>
> const otpVerification = await sendSmsOTP({ isoCountryCode: 'US', phoneNumber: '5551234567' });
>
> await verifyOTP({
>   otpVerification,
>   verificationToken: otpCode,
>   requestedScopes: [TokenScope.Credentiallink],
> });
> ```

> [tab] 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.
>
> ```javascript theme={"system"}
> import { verifyWalletAccount, getWalletAccounts, TokenScope } from '@dynamic-labs-sdk/client';
>
> const walletAccount = getWalletAccounts()[0];
> await verifyWalletAccount({
>   walletAccount,
>   requestedScopes: [TokenScope.Walletexport],
> });
> ```

> [tab] Social (OAuth)
>
> Social re-authentication uses the same OAuth redirect flow as social sign-in. Pass `requestedScopes` to `completeSocialRedirect` when the user returns.
>
> ```javascript theme={"system"}
> import {
>   signInWithSocialRedirect,
>   completeSocialRedirect,
>   ProviderEnum,
>   TokenScope,
> } from '@dynamic-labs-sdk/client';
>
> // 1. Redirect the user to the social provider
> await signInWithSocialRedirect({
>   provider: ProviderEnum.Google,
>   redirectUrl: window.location.href,
> });
>
> // 2. After redirect, complete the flow and request the elevated token
> const url = new URL(window.location.href);
> await completeSocialRedirect({
>   url,
>   requestedScopes: [TokenScope.Credentiallink],
> });
> ```

> [tab] Passkey MFA
>
> ```javascript theme={"system"}
> import { authenticatePasskeyMFA, TokenScope } from '@dynamic-labs-sdk/client';
>
> // Browser prompts for passkey (biometric, PIN, or security key)
> await authenticatePasskeyMFA({
>   requestedScopes: [TokenScope.Walletexport],
> });
> ```

> [tab] TOTP MFA
>
> ```javascript theme={"system"}
> import { authenticateTotpMfaDevice, TokenScope } from '@dynamic-labs-sdk/client';
>
> await authenticateTotpMfaDevice({
>   code: totpCode,
>   requestedScopes: [TokenScope.Walletexport],
> });
> ```

> [tab] Recovery Code
>
> ```javascript theme={"system"}
> import { authenticateMfaRecoveryCode, TokenScope } from '@dynamic-labs-sdk/client';
>
> await authenticateMfaRecoveryCode({
>   code: recoveryCode,
>   requestedScopes: [TokenScope.Credentialunlink],
> });
> ```

## Choosing the right method

The JavaScript SDK requires you to choose which verification method to present. `checkStepUpAuth` returns all available credentials — both MFA (passkey, TOTP) and re-auth (email, SMS, OAuth, wallet) — so you can route the user to the right method in a single call:

```javascript theme={"system"}
import {
  checkStepUpAuth,
  JwtVerifiedCredentialFormatEnum,
  TokenScope,
} from '@dynamic-labs-sdk/client';

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

  if (!isRequired) return null; // No step-up needed

  // Use the default credential if available, otherwise pick the first
  const credential =
    credentials.find((c) => c.id === defaultCredentialId) ?? credentials[0];

  switch (credential?.format) {
    case JwtVerifiedCredentialFormatEnum.Passkey:
      return 'passkey'; // Use authenticatePasskeyMFA
    case JwtVerifiedCredentialFormatEnum.Totp:
      return 'totp'; // Use authenticateTotpMfaDevice (collect code first)
    case JwtVerifiedCredentialFormatEnum.Email:
      return 'email'; // Use sendEmailOTP + verifyOTP
    case JwtVerifiedCredentialFormatEnum.PhoneNumber:
      return 'phone'; // Use sendSmsOTP + verifyOTP
    case JwtVerifiedCredentialFormatEnum.Oauth:
      return 'oauth'; // Use signInWithSocialRedirect + completeSocialRedirect
    case JwtVerifiedCredentialFormatEnum.Blockchain:
      return 'wallet'; // Use verifyWalletAccount
    default:
      return credential?.format;
  }
};
```

When a user has MFA methods registered, the backend requires MFA for elevated token issuance — re-auth credentials won't be returned. `checkStepUpAuth` handles this automatically, so you don't need separate logic for MFA vs. re-auth users.

> [!TIP]
>
> In React, drive each authentication step with its hook — `useCheckStepUpAuth` (or call `checkStepUpAuth` inside a handler), `useAuthenticatePasskeyMFA`, `useAuthenticateTotpMfaDevice`, `useSendEmailOTP`, and `useVerifyOTP` — and use `useState` to track which verification UI to show. The flow mirrors the full example below.

## Full example: wallet export with method selection

```javascript theme={"system"}
import {
  checkStepUpAuth,
  authenticatePasskeyMFA,
  authenticateTotpMfaDevice,
  sendEmailOTP,
  verifyOTP,
  JwtVerifiedCredentialFormatEnum,
  TokenScope,
} from '@dynamic-labs-sdk/client';

const SCOPE = TokenScope.Walletexport;

/**
 * Step-up verification with automatic method selection.
 * Uses checkStepUpAuth to get available credentials and the
 * recommended default credential to authenticate with.
 * `email` is the user's verified email address, needed to send an email OTP.
 */
const stepUp = async ({ email }) => {
  const { isRequired, credentials, defaultCredentialId } =
    await checkStepUpAuth({ scope: SCOPE });

  if (!isRequired) return { done: true };

  // Use the recommended default credential, or fall back to the first
  const credential =
    credentials.find((c) => c.id === defaultCredentialId) ?? credentials[0];

  if (!credential) return { done: false, needs: 'unknown' };

  // MFA credentials
  if (credential.format === JwtVerifiedCredentialFormatEnum.Totp) {
    return { done: false, needs: 'totp', deviceId: credential.id };
  }

  if (credential.format === JwtVerifiedCredentialFormatEnum.Passkey) {
    await authenticatePasskeyMFA({ requestedScopes: [SCOPE] });
    return { done: true };
  }

  // Re-auth credentials (email, SMS, wallet, social)
  if (credential.format === JwtVerifiedCredentialFormatEnum.Email) {
    const otpVerification = await sendEmailOTP({ email });
    return { done: false, needs: 'emailOtp', otpVerification };
  }

  return { done: false, needs: credential.format };
};

/** Call after collecting the code from the user. */
const completeStepUp = async ({ code, method, deviceId, otpVerification }) => {
  if (method === 'totp') {
    await authenticateTotpMfaDevice({
      code,
      deviceId,
      requestedScopes: [SCOPE],
    });
    return;
  }

  await verifyOTP({
    otpVerification,
    requestedScopes: [SCOPE],
    verificationToken: code,
  });
};
```

## Error handling

Verification methods throw when the user cancels or verification fails:

```javascript theme={"system"}
import { authenticatePasskeyMFA, TokenScope } from '@dynamic-labs-sdk/client';

try {
  await authenticatePasskeyMFA({ requestedScopes: [TokenScope.Walletexport] });
} catch (error) {
  // User cancelled the passkey prompt, or verification failed
  console.error('Step-up failed:', error.message);
}
```

Common error scenarios:

* User cancels the browser passkey prompt
* Invalid OTP code entered
* MFA device verification timeout
* Network error during verification

## Available scopes

See the [complete scopes reference](https://docs.dynamic.xyz/docs/auth/step-up-auth.md#scopes) for all supported values.

## External auth (Bring Your Own Auth)

If you use [external auth (BYOA)](https://docs.dynamic.xyz/docs/auth/bring-your-own-auth.md), your backend can issue elevated access tokens directly — no user interaction required. See the [External Auth Step-Up guide](https://docs.dynamic.xyz/docs/javascript/authentication-methods/step-up-auth/external-auth.md).

## Related

* [Step-Up Authentication Overview](https://docs.dynamic.xyz/docs/auth/step-up-auth.md) — Concepts, scopes, token lifecycle
* [External Auth Step-Up](https://docs.dynamic.xyz/docs/javascript/authentication-methods/step-up-auth/external-auth.md) — Backend-issued elevated tokens for BYOA
* [Passkey MFA](https://docs.dynamic.xyz/docs/javascript/authentication-methods/mfa/passkey.md)
* [Authenticator Apps (TOTP)](https://docs.dynamic.xyz/docs/javascript/authentication-methods/mfa/totp.md)


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