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 verificationNOT: 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 passwordHera 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.
When email link is enough
- B2C consumer apps (Spotify, Notion, Slack default to magic link).
- Low-value B2B (notes, productivity).
- Most SaaS.
When email link isn't enough
- 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: trueUser 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:
- Detect they don't have a passkey.
- After successful password login, prompt:
Switch to passwordless sign-in? Faster, more secure, no passwords to remember. [Save passkey] - Save passkey. Continue.
- Next login: passkey is default; password is "use a different method" link.
- 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:
- Wrong RP ID (see troubleshooting, passkey not prompted).
- User cleared keychain.
- Different browser/device.
Don't trap user. Show "Use email instead" prominently.