Early access — hidden. Login MFA isn’t in the Para Modal yet, so there’s no built-in UI for it. You integrate it with your own UI today (React hooks or fully custom). Reach out to your Para contact to enable it on your project.
This is login-time MFA — a second factor on every sign-in. It’s distinct from Para’s recovery 2FA (
useSetup2fa / useVerify2fa), which gates account recovery. The hooks and methods below (enrollMfa / verifyMfa) are login MFA only.How it works
When login MFA is owed, the SDK pauses the login on a challenge and surfaces it through the auth state. There are two challenge phases:| Auth phase | Meaning | What your UI does |
|---|---|---|
awaiting_2fa_enrollment | First time — the user has no factor yet | Call enrollMfa to mint a secret, render the QR, show backup codes, then collect the first code |
awaiting_2fa | Returning — the user already has a factor | Prompt for a code |
Before you start
- Para has enabled login MFA on your project (see Enabling login MFA).
- The Para React SDK (
@getpara/react-sdk) or Web SDK (@getpara/web-sdk), v3. - A custom login UI — login MFA has no Para Modal UI yet, so you render the challenge yourself.
Enabling login MFA
Login MFA is configured on your project’s partner record by the Para team — it isn’t self-serve while the feature is in early access. Reach out to your Para contact and tell them which mode you want:| Mode | Behavior |
|---|---|
optional | Only users who have enrolled a factor are challenged. New users aren’t forced to set one up. |
required | Every user must set up a factor and pass it on each login. |
disabled | Off (the default). |
The CLI/Developer Portal “Enable two-factor authentication” toggle controls recovery 2FA, not login MFA — don’t use it for this. Login MFA is enabled separately by Para.
Integrate it
Pick the path that matches how you build your login UI. Both drive the sameenrollMfa / verifyMfa flow — the hooks just wrap it in React Query state.
- React hooks
- Fully custom UI (Web SDK)
Use
useEnrollMfa and useVerifyMfa (from @getpara/react-sdk) together with the auth-state subscription. This mirrors the Custom OIDC example — a small hook surfaces the challenge from the SDK state and drives it.A challenge hook
Subscribe to the auth state to detect the hold, fetch the enrollment secret once, and expose averify function:Render the challenge
Render a QR from theuri (any QR component works), show the backup codes once during enrollment, and collect a 6-digit code:Backup codes
enrollMfa returns a set of one-time backup codes alongside the QR uri. Show them to the user once, during setup, and tell them to store the codes somewhere safe — they’re how a user gets in if they lose their authenticator. Para stores only hashes and never shows them again.
A backup code is accepted anywhere a TOTP code is — pass it to verifyMfa({ code }) exactly the same way. Each backup code works once.
Handling wrong codes and lockout
verifyMfa returns { ok: false, attemptsRemaining } for an incorrect code. Surface attemptsRemaining and let the user try again — don’t treat it as a fatal error.
After too many wrong codes the session is locked out; the user must restart the login (a fresh sign-in resets the budget). When attemptsRemaining reaches 0, prompt the user to sign in again.
Security
- The TOTP secret is generated server-side and stored encrypted — your UI only ever receives the
otpauth://provisioningurito render. - Backup codes are single-use and stored as hashes; they’re returned in plaintext only once, at enrollment.
- A correct factor is what releases the user’s wallet keyshares for the session — the gate is enforced on Para’s backend, not in your UI, so it can’t be bypassed client-side.
Troubleshooting
The login never shows a 2FA challenge
The login never shows a 2FA challenge
Confirm Para has enabled login MFA on your project (
optional or required). In optional mode, only users who have already enrolled a factor are challenged — a brand-new user won’t be unless the mode is required. Also make sure your UI subscribes to onStatePhaseChange and renders on awaiting_2fa / awaiting_2fa_enrollment.enrollMfa returns a new QR every time / duplicate codes
enrollMfa returns a new QR every time / duplicate codes
Each
enrollMfa() call mints a fresh secret and a new set of backup codes. Call it once per enrollment challenge (guard it, as the example does with a ref) so the QR and backup codes are stable while the user scans them.A correct code doesn't advance the login
A correct code doesn't advance the login
On
{ ok: true } the SDK re-polls the login itself — don’t also re-trigger authentication. If you’re keeping the auth popup/window open, make sure it stays open until the wallet connects.Codes are always rejected
Codes are always rejected
TOTP codes are time-based — make sure the user’s device clock is accurate. Para accepts a small skew window. If they’ve lost their authenticator, have them use a backup code instead.
Next steps
Build a Custom UI
The full custom-login flow the MFA challenge plugs into.
Custom OIDC
Add your own identity provider as a first factor.