Cross-device passkey login
Phone unlocks desktop, the WebAuthn way
A user has a passkey on their phone. They want to log in on a desktop browser that doesn't have a passkey of its own. WebAuthn's "hybrid" transport (also called CTAP2 / caBLE) makes this work.
How it looks
- User clicks "Sign in with passkey" on desktop.
- Browser shows QR code.
- User scans with phone camera.
- Phone prompts biometric.
- Approval signed by phone, transmitted to desktop via Bluetooth proximity check.
- Desktop session created.
No passkey synced to desktop. No password.
Browser/OS requirements
- Phone: iOS 16+, Android 9+ (most modern devices).
- Desktop: Chrome 108+, Safari 16+, Edge 108+, Firefox 119+.
- Both devices: Bluetooth on. They don't need to be on the same network.
In Hera / Kratos
Kratos's WebAuthn flow already supports hybrid. No special config, the browser handles transport negotiation. From Olympus's side, you just enable WebAuthn:
# kratos.yml
selfservice:
methods:
webauthn:
enabled: true
config:
rp:
display_name: Your App
id: your-domain.com
passwordless: truepasswordless: true lets WebAuthn complete a login flow standalone (no password first).
User flow
When the user clicks "Sign in with passkey":
<button id="passkey-login">Sign in with passkey</button>
<script>
document.getElementById("passkey-login").addEventListener("click", async () => {
const options = await fetch("/self-service/login/api?aal=aal1&method=passkey", {
credentials: "include"
}).then(r => r.json());
const cred = await navigator.credentials.get({
publicKey: parsePublicKeyOptions(options),
mediation: "optional",
});
// Browser handles QR / Bluetooth dance internally.
// Submit assertion to Kratos.
await fetch(options.ui.action, {
method: "POST",
credentials: "include",
body: JSON.stringify({
method: "passkey",
passkey_login: serializeCredential(cred),
}),
});
});
</script>The browser, not your code, displays the QR code and orchestrates the cross-device step. You just call navigator.credentials.get with mediation: "optional" to allow the conditional UI.
Roaming authenticator vs platform
- Roaming: USB security key (YubiKey), or phone via hybrid. Works across devices.
- Platform: tied to the device (Touch ID/Face ID on Mac, Windows Hello on PC).
authenticatorSelection.authenticatorAttachment controls preference:
authenticatorSelection: {
authenticatorAttachment: "cross-platform", // wants roaming / hybrid
// or "platform", limits to device's own biometrics
// or omit, let browser choose
}For "sign in on a new desktop with my phone," you want cross-platform OR no preference.
Enrolling cross-device
A user can enroll a passkey from their phone while they're signing in on desktop:
- Desktop login → password auth.
- Settings → "Add passkey."
- Choose "Use a phone or tablet" in the OS dialog.
- QR on desktop → scan with phone.
- Phone biometric → passkey created on phone, attached to user.
- Next time: desktop login → "Sign in with passkey" → same QR/Bluetooth dance.
Troubleshooting
QR scans but Bluetooth fails.
- Both devices need Bluetooth on.
- iCloud Keychain / Google Password Manager sync state matters.
- Older devices may not support the proximity check.
No QR appears, only "use security key."
- Browser is older.
- OR
authenticatorSelection.authenticatorAttachment: "platform"was set, excluding hybrid.
"Could not verify your identity" after biometric.
- Origin mismatch:
rp.idin Kratos config doesn't match the domain the user is on. - Set
rp.id: your-domain.com(no subdomain) for it to work across subdomains.
UX considerations
- Add a fallback: "Sign in another way" link below the passkey button. Goes to password.
- First-time users won't know what "passkey" is. Use language like "Sign in with phone biometric" or "Sign in without password."
- Cross-device adds 5-10s vs same-device. Worth it for new-desktop scenarios.