Introduction
The Basics
- Login / Signup
- Chains / Networks
- Embedded Wallets
- Server Wallets
- Smart Accounts (AA)
- External Wallets
- Users / VC's
- Design
- Money & Funding
Beyond The Basics
- Using Wallets
- Headless
- Global Identity
- Global Wallets
- Wallet Connect Global Connectivity
- Bridge Widget
- Rate Limits
Developer Dashboard
- SDK and API Keys
- Sandbox vs Live
- Analytics
- User Management
- Test Accounts
- Settings
- Admin
- Webhooks
- Configuring Social Providers
Migrating to Dynamic
- Migrating to Dynamic
- Migration Tutorials
For Wallets & Chains
Hackathons
Legacy Embedded Wallets
Headless MFA with Passkey
Passkeys MFA is still in close beta. Please contact us to request access to enable this feature for your app.
Introduction
This guide will show you how to add a passkey for MFA to your app using your own custom UI. You can also refer to the general MFA guide here to learn more about this feature (note that this guide is for account level MFA, rather than transaction level MFA).
General Flow
When user already has a passkey registered
- User logs in
- User is redirected to the MFA view
- User adds a passkey
- User is redirected to the recovery codes view
- User acknowledges the recovery codes
The user must acknowledge the recovery codes before the MFA setup flow is complete.
When user does not have a passkey registered
- User logs in
- User is redirected to the MFA view
- User selects existing passkey to authenticate
User does not need to acknowledge the recovery codes again in this flow.
Implementation 1: using our useSyncMfaFlow hook
We provide a hook that will use a provided handler to automatically trigger the MFA flow when required.
import {
useDynamicContext,
useSyncMfaFlow,
useRegisterPasskey,
useAuthenticatePasskeyMFA,
useMfa,
useGetPasskeys,
} from "@dynamic-labs/sdk-react-core";
export const MfaComponent = () => {
const { userWithMissingInfo } = useDynamicContext();
const { getRecoveryCodes } = useMfa();
const getPasskeys = useGetPasskeys();
const registerPasskey = useRegisterPasskey();
const authenticatePasskeyMFA = useAuthenticatePasskeyMFA();
useSyncMfaFlow({
// this handler will run in one of these three scenarios:
// 1. when user is required to register a passkey on login
// 2. when user has a registered passkey and is required to authenticate after login
// 3. when user is still pending recovery codes acknowledgement
handler: async () => {
// this checks if the user is pending registration or authentication of a passkey
if (userWithMissingInfo?.scope?.includes("requiresAdditionalAuth")) {
getPasskeys().then(async (passkeys) => {
// if the user has no passkeys, we need to register a new one
if (passkeys.length === 0) {
// here you can show a view to display while the user is prompted to register a passkey
// e.g. setView('passkey-registration')
await registerPasskey();
} else {
// if the user has a passkey, we need to authenticate it
// here you can show a view to display while the user is prompted to authenticate their passkey
// e.g. setView('passkey-authentication')
await authenticatePasskeyMFA({
createMfaToken: {
singleUse: true,
},
});
}
});
} else {
// if the user already has a passkey registered and authenticated,
// but is pending recovery codes acknowledgement
const recoveryCodes = await getRecoveryCodes();
// here you can show a view to display the recovery codes with a button to acknowledge them
// to acknowledge the recovery codes, you can call the `completeAcknowledgement()` function
// e.g. setView('recovery-codes', { recoveryCodes, onAck: completeAcknowledgement })
}
},
});
};
Implementation 2: you decide when to trigger each step of the MFA flow
If you set MFA as required for your environment, the user wonโt be considered fully authenticated until they have completed the entire MFA flow.
import { UserPasskey } from "@dynamic-labs/sdk-api-core";
import {
useAuthenticatePasskeyMFA,
useGetPasskeys,
useIsLoggedIn,
useMfa,
useRegisterPasskey,
} from "@dynamic-labs/sdk-react-core";
import { useCallback, useEffect, useState } from "react";
export const MfaComponent = () => {
// when the user is pending any MFA action, Dynamic will set `userWithMissingInfo`
// other wise it will be undefined and `user` will be defined instead
const isLogged = useIsLoggedIn();
const { getRecoveryCodes, completeAcknowledgement } = useMfa();
const getPasskeys = useGetPasskeys();
const registerPasskey = useRegisterPasskey();
const authenticatePasskeyMFA = useAuthenticatePasskeyMFA();
const [error, setError] = useState<string>();
const [userPasskeys, setUserPasskeys] = useState<UserPasskey[]>([]);
const [recoveryCodes, setRecoveryCodes] = useState<string[]>([]);
// refresh the user's passkeys
const refreshUserPasskeys = useCallback(async () => {
const passkeys = await getPasskeys();
setUserPasskeys(passkeys);
}, [getPasskeys]);
// fetch passkeys when the user is logged in
// so you can display them in your UI
useEffect(() => {
if (isLogged) {
refreshUserPasskeys();
}
}, [isLogged, refreshUserPasskeys]);
// register a new passkey
const handleRegisterPasskey = async () => {
// here you can show a view to display while the user is prompted to register a passkey
// e.g. setView('passkey-registration')
try {
await registerPasskey();
// if you want to refresh the user's passkeys after registering a new one
refreshUserPasskeys();
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
}
};
const handleAuthenticatePasskey = async () => {
// here you can show a view to display while the user is prompted to authenticate their passkey
// e.g. setView('passkey-authentication')
try {
// if you pass in a createMfaToken option as shown below,
// `authenticatePasskeyMFA` will return a single use MFA token
const mfaAuthToken = await authenticatePasskeyMFA({
createMfaToken: {
singleUse: true,
},
});
console.log("mfaAuthToken", mfaAuthToken);
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
}
};
const handleRecoveryCodes = async () => {
try {
const recoveryCodes = await getRecoveryCodes();
// here you can show a view to display the recovery codes with a button to acknowledge them
// to acknowledge the recovery codes, you can call the `completeAcknowledgement()` function
// e.g. setView('recovery-codes', { recoveryCodes, onAck: completeAcknowledgement })
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
}
};
return (
<div className="headless-mfa-view">
<div className="headless-mfa-view__section">
<p>
<b>Passkeys</b>
</p>
{userPasskeys.map((passkey) => (
<div key={passkey.id}>
<p>Id: {passkey.id}</p>
<p>Alias: {passkey.alias}</p>
<p>Credential Id: {passkey.credentialId}</p>
<p>Origin: {passkey.origin}</p>
<p>Storage: {passkey.storage?.name}</p>
<p>Created At: {new Date(passkey.createdAt).toLocaleString()}</p>
<p>
Updated At:{" "}
{passkey.updatedAt
? new Date(passkey.updatedAt).toLocaleString()
: "N/A"}
</p>
</div>
))}
</div>
{error && <div className="headless-mfa__section error">{error}</div>}
{recoveryCodes && (
<div className="headless-mfa-view__section">
<p className="headless-mfa-view__codes">
Recovery Codes: {recoveryCodes.join(", ")}
</p>
</div>
)}
<div className="headless-mfa-view__section">
<button onClick={() => handleRegisterPasskey()}>
Register Passkey
</button>
</div>
<div className="headless-mfa-view__section">
<button onClick={() => handleAuthenticatePasskey()}>
Authenticate Passkey
</button>
</div>
<div className="headless-mfa-view__section">
<button onClick={() => handleRecoveryCodes()}>
Generate Recovery Codes
</button>
</div>
</div>
);
};
Was this page helpful?