Olympus Docs
CookbookMFA & step-up

Just-in-time MFA prompt

Ask user to enable MFA at a teachable moment

If you require MFA at signup, you'll lose conversions. If you never require, adoption is < 10%. Middle path: ask at the right moments.

Teachable moments

The user is paying attention and feels the value when:

  • They're about to do something important (first payment, first invite to a teammate).
  • They've recently been impacted by a related event (signed in from a new device).
  • They've completed a milestone (used the product for 7 days).
  • They've expressed care about security (toggled a "make this secure" setting).

These are NOT teachable:

  • The moment of signup (they want to see the app).
  • Right after creating an account (overwhelmed).
  • When they're about to leave.

Pattern

Soft prompt at teachable moment, dismissible:

{!user.has_mfa && shouldPromptMfa(user) && (
  <Modal>
    <h2>Add 2-factor authentication?</h2>
    <p>Your account holds sensitive data. 2FA protects against compromised passwords.</p>
    <p>Setup takes 2 minutes.</p>
    <Button onClick={enrollMfa}>Set up 2FA</Button>
    <Button variant="ghost" onClick={dismiss}>Maybe later</Button>
  </Modal>
)}

Trigger logic

function shouldPromptMfa(user) {
  if (user.has_mfa) return false;
  if (user.dismissed_mfa_recently) return false;
  
  const triggers = [
    user.actions_count > 10,         // engaged user
    user.created_at < 7daysAgo,      // not brand new
    user.last_payment_at,            // monetized
    user.invited_others_count > 0,   // collaborator
    user.security_event_recently,    // had a scare
  ];
  
  return triggers.filter(Boolean).length >= 2;
}

Multiple signals = clearer benefit.

Dismissal tracking

CREATE TABLE mfa_prompts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  identity_id UUID NOT NULL,
  prompted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  response TEXT NOT NULL,  -- "enrolled" | "dismissed" | "later"
  reason TEXT
);

After 3 dismissals, escalate? Or give up?

Track conversion rate. If "dismissed" rate is high, the prompt isn't working, change wording or moment.

"Later" with reminder

<Button onClick={dismissAndRemind}>Remind me in a week</Button>
async function dismissAndRemind() {
  await db.insert(reminders).values({
    identity_id: user.id,
    type: "mfa",
    remind_at: addDays(new Date(), 7),
  });
}

Cron picks up; emails reminder.

Force at threshold

For some milestones, prompt becomes mandatory:

function mustEnrollMfa(user, action) {
  if (action === "transfer_money_over_1000") return true;
  if (action === "delete_account") return true;
  if (action === "view_compliance_data") return true;
  return false;
}
if (mustEnrollMfa(user, action) && !user.has_mfa) {
  return <ForceMfaEnrollment />;
}

User can't proceed without enrolling.

Frame it positively

Bad: "You must enable 2FA." Good: "Add 2FA to protect your account. Takes 2 minutes."

Bad: "Required for compliance." Good: "Your data is sensitive. Let's add an extra layer."

People comply more for self-interest than compliance.

Choose method UX

When user clicks "enroll," offer choices:

<RadioGroup>
  <Radio value="webauthn">
    Use this device's biometric (Face ID / Touch ID)
    <p>Most convenient.</p>
  </Radio>
  <Radio value="totp">
    Authenticator app (Google Authenticator, Authy, 1Password)
    <p>Works without internet.</p>
  </Radio>
  <Radio value="sms">
    Text message
    <p>Easy but less secure.</p>
  </Radio>
</RadioGroup>

Default to WebAuthn / passkey if device supports.

Don't make MFA scary

"To protect your account, scan this QR code with your authenticator app:"

[QR]

"After scanning, enter the 6-digit code shown in your app."

[Input]

Not:

"Generate a TOTP shared secret keyed with HMAC-SHA1 and provision a..." 

Plain language.

After enrollment

Celebrate:

<Modal>
  <h2>✓ 2-factor enabled</h2>
  <p>Your account is now significantly more secure.</p>
  
  <h3>Save your recovery codes</h3>
  <p>If you lose your authenticator, use one of these codes:</p>
  <CodeList codes={recoveryCodes} />
  
  <Checkbox label="I've saved my codes" />
  <Button>Done</Button>
</Modal>

Reward, not chore.

Analytics

Track:

  • Prompts shown.
  • Dismissed (by reason if asked).
  • Enrolled.
  • After enrollment, MFA challenges per session.

If enrollment is high but challenge-success is low: the UX is broken, TOTP codes not working, etc.

Don't promote SMS

SMS MFA has known weaknesses (SIM swap). Promote stronger:

<Radio value="sms" disabled={highSecurityAccount}>
  SMS (not recommended for high-value accounts)
</Radio>

For admin / financial: hide SMS.

On this page