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

# 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](/overview/authentication/step-up-auth).

## Prerequisites

* `@dynamic-labs-sdk/client` initialized
* At least one verification method enabled in your [dashboard security settings](https://app.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,
} from '@dynamic-labs-sdk/client';
import { TokenScope } from '@dynamic-labs/sdk-api-core';

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 } from '@dynamic-labs-sdk/client';
import { TokenScope } from '@dynamic-labs/sdk-api-core';

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"}
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 `hasElevatedAccessToken` only if you need a simple synchronous local-only check.
</Note>

Use `hasElevatedAccessToken` to check if the user already has a non-expired elevated token for a scope:

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

if (hasElevatedAccessToken(TokenScope.Walletexport)) {
  // 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.

<Tabs>
  <Tab title="Email OTP">
    ```javascript theme={"system"}
    import { sendEmailOTP, verifyOTP } from '@dynamic-labs-sdk/client';
    import { TokenScope } from '@dynamic-labs/sdk-api-core';

    // 1. Send the OTP to the user's verified email
    await sendEmailOTP();

    // 2. Verify with the code the user entered
    await verifyOTP({
      type: 'email',
      token: otpCode,
      requestedScopes: [TokenScope.Credentiallink],
    });
    // Token is now stored and will be applied automatically
    ```
  </Tab>

  <Tab title="SMS OTP">
    ```javascript theme={"system"}
    import { sendSmsOTP, verifyOTP } from '@dynamic-labs-sdk/client';
    import { TokenScope } from '@dynamic-labs/sdk-api-core';

    await sendSmsOTP({ countryCode: 'US', phoneNumber: '5551234567' });

    await verifyOTP({
      type: 'sms',
      token: otpCode,
      requestedScopes: [TokenScope.Credentiallink],
    });
    ```
  </Tab>

  <Tab title="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 { verifyWallet } from '@dynamic-labs-sdk/client';
    import { TokenScope } from '@dynamic-labs/sdk-api-core';

    // walletId is optional — defaults to the first connected wallet when omitted
    await verifyWallet({
      walletId: 'wallet-id',
      requestedScopes: [TokenScope.Walletexport],
    });
    ```
  </Tab>

  <Tab title="Social (OAuth)">
    ```javascript theme={"system"}
    import { verifySocial } from '@dynamic-labs-sdk/client';
    import { ProviderEnum, TokenScope } from '@dynamic-labs/sdk-api-core';

    // Opens a popup for the user to re-authenticate with their linked social account
    await verifySocial({
      provider: ProviderEnum.Google,
      requestedScopes: [TokenScope.Credentiallink],
    });
    ```
  </Tab>

  <Tab title="Passkey MFA">
    ```javascript theme={"system"}
    import { authenticatePasskeyMFA } from '@dynamic-labs-sdk/client';
    import { TokenScope } from '@dynamic-labs/sdk-api-core';

    // Browser prompts for passkey (biometric, PIN, or security key)
    await authenticatePasskeyMFA({
      requestedScopes: [TokenScope.Walletexport],
    });
    ```
  </Tab>

  <Tab title="TOTP MFA">
    ```javascript theme={"system"}
    import { authenticateTotpMfaDevice } from '@dynamic-labs-sdk/client';
    import { TokenScope } from '@dynamic-labs/sdk-api-core';

    await authenticateTotpMfaDevice({
      code: totpCode,
      requestedScopes: [TokenScope.Walletexport],
    });
    ```
  </Tab>

  <Tab title="Recovery Code">
    ```javascript theme={"system"}
    import { authenticateMfaRecoveryCode } from '@dynamic-labs-sdk/client';
    import { TokenScope } from '@dynamic-labs/sdk-api-core';

    await authenticateMfaRecoveryCode({
      code: recoveryCode,
      requestedScopes: [TokenScope.Credentialunlink],
    });
    ```
  </Tab>
</Tabs>

## 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 } from '@dynamic-labs-sdk/client';
import {
  JwtVerifiedCredentialFormatEnum,
  TokenScope,
} from '@dynamic-labs/sdk-api-core';

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 verifySocial
    case JwtVerifiedCredentialFormatEnum.Blockchain:
      return 'wallet'; // Use verifyWallet
    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.
</Tip>

## Full example: wallet export with method selection

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

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.
 */
const stepUp = async () => {
  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) {
    await sendEmailOTP();
    return { done: false, needs: 'emailOtp' };
  }

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

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

## Error handling

Verification methods throw when the user cancels or verification fails:

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

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](/overview/authentication/step-up-auth#scopes) for all supported values.

## External auth (Bring Your Own Auth)

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

## Related

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