Skip to main content

Overview

Passkey MFA lets you require users to verify their identity with a passkey after their primary login factor completes — whether that’s email OTP or OAuth. The passkey acts as a second factor using the device’s biometric sensor (fingerprint, Face ID) or PIN, providing phishing-resistant security on top of the primary authentication method. When a user has both TOTP MFA and passkey MFA enabled, they can choose which method to use at login time. Recovery codes are issued at enrollment so users can regain access if they lose their device.

Compatibility

Passkey MFA is supported for the loginWithEmailOTP and OAuth flows. It requires the Passkey extension and is available on the Web SDK only.

Setup

1. Enable in the dashboard

Enable Passkey MFA in your Magic dashboard before calling any MFA methods. Users will not be able to enroll until this setting is turned on.

2. Register a passkey

The user must have at least one passkey registered before they can enable passkey MFA. Use registerNewUser for new users or addPasskey for existing logged-in users.

3. Enable passkey MFA

Call enablePasskeyMfa() while the user has an active session. On success, Magic returns a set of one-time recovery codes — display these to the user immediately, as they cannot be retrieved again.
JavaScript
import { Magic } from 'magic-sdk';
import { PasskeyExtension } from '@magic-ext/passkey';

const magic = new Magic('YOUR_API_KEY', {
  extensions: [new PasskeyExtension()],
});

const { recoveryCodes } = await magic.passkey.enablePasskeyMfa();

// Show recovery codes to the user — they won't be shown again
console.log('Save these recovery codes:', recoveryCodes);
Return value
TypeScript
interface EnablePasskeyMfaResult {
  recoveryCodes: string[];
}

Login with passkey MFA

Magic-hosted UI (default)

No additional code is required. Magic automatically presents the passkey verification step after the primary factor completes — whether that’s email OTP or an OAuth redirect.
const didToken = await magic.auth.loginWithEmailOTP({ email: '[email protected]' });

Custom UI

When you control the UI, pass showUI: false (email OTP) or showMfaModal: false (OAuth) to handle the MFA step yourself. The MFA events are the same for both flows.

Email OTP

Add MFA events to your existing email OTP event handler:
JavaScript
import {
  Magic,
  LoginWithEmailOTPEventOnReceived,
  LoginWithEmailOTPEventEmit,
  MfaEventOnReceived,
  MfaEventEmit,
} from 'magic-sdk';
import { PasskeyExtension } from '@magic-ext/passkey';

const magic = new Magic('YOUR_API_KEY', {
  extensions: [new PasskeyExtension()],
});

const handle = magic.auth.loginWithEmailOTP({ email: '[email protected]', showUI: false });

handle
  // --- Email OTP step ---
  .on(LoginWithEmailOTPEventOnReceived.EmailOTPSent, () => {
    const otp = window.prompt('Enter the code sent to your email');
    handle.emit(LoginWithEmailOTPEventEmit.VerifyEmailOtp, otp);
  })

  // --- Passkey MFA step ---

  // Only emitted when the user has both TOTP and passkey MFA enabled
  .on(MfaEventOnReceived.SelectMfaType, () => {
    const type = window.confirm('Verify with passkey? Click Cancel to use your authenticator app instead.')
      ? 'passkey'
      : 'totp';
    handle.emit(MfaEventEmit.SelectedMfaType, type);
  })

  // Emitted when the passkey challenge is ready — the browser handles the WebAuthn prompt
  .on(MfaEventOnReceived.MfaSentHandle, () => {
    // No action required. Emit LostDevice if the user cannot access their passkey.
    // handle.emit(MfaEventEmit.LostDevice);
  })

  // Recovery code flow (triggered after emitting LostDevice)
  // Follows the same pattern as TOTP MFA recovery — see the MFA docs for full event handling

  .on('done', (result) => {
    console.log('Login complete, DID token:', result);
  })
  .on('error', (err) => {
    console.error('Login failed:', err);
  });

await handle;

OAuth

Pass showMfaModal: false to getRedirectResult and attach the same MFA events:
JavaScript
import {
  Magic,
  MfaEventOnReceived,
  MfaEventEmit,
} from 'magic-sdk';
import { OAuthExtension } from '@magic-ext/oauth2';
import { PasskeyExtension } from '@magic-ext/passkey';

const magic = new Magic('YOUR_API_KEY', {
  extensions: [new OAuthExtension(), new PasskeyExtension()],
});

const handle = magic.oauth2.getRedirectResult({ showMfaModal: false });

handle
  // Only emitted when the user has both TOTP and passkey MFA enabled
  .on(MfaEventOnReceived.SelectMfaType, () => {
    const type = window.confirm('Verify with passkey? Click Cancel to use your authenticator app instead.')
      ? 'passkey'
      : 'totp';
    handle.emit(MfaEventEmit.SelectedMfaType, type);
  })

  // Emitted when the passkey challenge is ready — the browser handles the WebAuthn prompt
  .on(MfaEventOnReceived.MfaSentHandle, () => {
    // No action required. Emit LostDevice if the user cannot access their passkey.
    // handle.emit(MfaEventEmit.LostDevice);
  })

  // Recovery code flow (triggered after emitting LostDevice)
  // Follows the same pattern as TOTP MFA recovery — see the MFA docs for full event handling

  .on('done', (result) => {
    console.log('OAuth login complete:', result);
  })
  .on('error', (err) => {
    console.error('Login failed:', err);
  });

await handle;
MFA event reference
EventDirectionDescription
MfaEventOnReceived.SelectMfaTypeReceivedEmitted when the user has both TOTP and passkey MFA — they must choose one
MfaEventEmit.SelectedMfaTypeEmitSend "passkey" or "totp" to select the MFA method
MfaEventOnReceived.MfaSentHandleReceivedPasskey challenge is active — browser shows the WebAuthn prompt automatically
MfaEventEmit.LostDeviceEmitSignal that the user cannot use their passkey — initiates recovery code flow

Disable passkey MFA

Call disablePasskeyMfa() while the user has an active session.
JavaScript
await magic.passkey.disablePasskeyMfa();

Account recovery

When a user cannot access their passkey, they can use a recovery code to complete login. Recovery codes are single-use and are issued when the user first calls enablePasskeyMfa(). Make sure your UI prompts users to save them at enrollment time. After a recovery code is used, the user’s passkey MFA is deactivated. They can re-enroll by calling enablePasskeyMfa() again. If a user loses both their passkey and their recovery codes, you can disable MFA on their behalf from the Users section of the Magic dashboard.