Olympus Docs
CookbookAuth flows

Passwordless with secure fallback

Default to passkey, fall back gracefully when needed

You're pushing users to passkeys / WebAuthn. But not every user can use them yet (old device, locked-out scenarios, etc.). You need a sensible fallback that's also secure.

The path

Default: Passkey (WebAuthn)
   ↓ (if passkey fails)
Fallback 1: Email magic link
   ↓ (if email is compromised / lost)
Fallback 2: Admin verification

NOT: passkey → password. Password defeats the purpose.

Configuration

# kratos.yml
selfservice:
  methods:
    webauthn:
      enabled: true
      config:
        passwordless: true
    code:
      enabled: true   # magic link / one-time code
      config:
        lifespan: 15m
    password:
      enabled: false  # no password

Hera UI

Login page:

<h1>Sign in</h1>

<button onClick={startPasskey}>Sign in with passkey</button>

<details>
  <summary>Can't use passkey?</summary>
  <p>Get a sign-in link by email:</p>
  <input type="email" placeholder="Email" />
  <button>Send link</button>
</details>

Primary: passkey. Secondary: email link, behind a disclosure.

Why this is OK

The secondary path is single-factor (email possession). For most apps this is sufficient, comparable to password reset access.

For high-value accounts, require step-up via passkey re-enrollment immediately after email-link sign-in.

  • B2C consumer apps (Spotify, Notion, Slack default to magic link).
  • Low-value B2B (notes, productivity).
  • Most SaaS.
  • Financial (banking, brokerage).
  • Healthcare (HIPAA).
  • High-value B2B (admin of large customers).

For these:

  • Require passkey re-enrollment after email-link sign-in.
  • Until re-enrolled, restrict to low-stakes operations.

"Locked out" recovery

User loses their passkey device, can't access email. What now?

Option A: Admin recovery

User contacts support. Support verifies identity out-of-band (call back to known phone, video call, ID upload). Then admin issues a one-time recovery link.

async function issueRecoveryLink(identityId: string, adminId: string) {
  await db.insert(supportRecoveries).values({
    identity_id: identityId,
    admin_id: adminId,
    reason: "passkey_lost_no_email",
  });
  const url = await kratos.adminCreateRecoveryLink(identityId);
  return url; // give to user via verified channel
}

Option B: Backup codes

At passkey enrollment, generate 10 one-time backup codes:

selfservice:
  methods:
    lookup_secret:
      enabled: true

User downloads / prints. Uses if locked out.

Encourage but don't require, many users won't save.

Option C: Multi-device passkeys

If user enrolled passkeys on phone AND laptop, losing one isn't lock-out. Strongly suggest enrolling 2+ devices.

UI Push for multi-device

After first passkey enroll:

<Step>
  ✓ Passkey saved to this device
  
  Save another passkey for a backup?
  [On phone]    [On hardware key]    [Later]
</Step>

Lower friction now (user is fresh) > later (forgot about it).

Comparing to "passkey + password"

Some sites offer both passkey AND password as alternatives. Discourage:

  • Password becomes a weak link the moment it's an option.
  • Attackers target the password.

Better: only passkey for new accounts; old accounts grandfather password but encouraged to switch.

Migration from password to passkey

For existing password users:

  1. Detect they don't have a passkey.
  2. After successful password login, prompt:
    Switch to passwordless sign-in?
    Faster, more secure, no passwords to remember.
    [Save passkey]
  3. Save passkey. Continue.
  4. Next login: passkey is default; password is "use a different method" link.
  5. After 30 days, ask "Disable password?", once disabled, no password fallback.

Gradual migration. Don't force.

When passkey fails at sign-in

User clicks "Sign in with passkey" → browser shows no passkey or fails.

Common causes:

Don't trap user. Show "Use email instead" prominently.

On this page