Credential phishing works because a password is a secret the user can be persuaded to type into the wrong place, and one-time codes inherit the same flaw — a convincing proxy page collects the code and replays it within the minute. Every awareness training programme in the world is an attempt to make humans compensate for a protocol weakness.
Passkeys remove the weakness rather than mitigating it. The credential is a key pair, the private half never leaves the authenticator, and the signature is bound to the origin by the browser. A user on a lookalike domain cannot authenticate to the real one, because the browser will not produce a signature for the wrong origin. There is nothing to type, so there is nothing to phish.
Synced versus device-bound
This is the decision that shapes your rollout. Synced passkeys live in a platform or password-manager keychain and follow the user across devices — dramatically better adoption, far fewer recovery tickets, and a security model that includes the user's cloud account. Device-bound passkeys, typically on a hardware key, never leave the device and give you a strong assurance level with a much higher operational burden.
For most enterprise SaaS, synced passkeys for everyone plus device-bound keys required for administrative roles is the balance that works. It also matches how breaches actually happen: the account you most need to protect is the one that can change other accounts.
// Registration: the server decides the policy, the browser enforces the origin.
const credential = await navigator.credentials.create({
publicKey: {
challenge: fromBase64(options.challenge), // single-use, server-generated
rp: { id: 'app.example.com', name: 'Example' },
user: { id: userHandle, name: email, displayName: fullName },
pubKeyCredParams: [{ alg: -7, type: 'public-key' }, // ES256
{ alg: -257, type: 'public-key' }], // RS256
authenticatorSelection: {
residentKey: 'required', // discoverable: enables usernameless sign-in
userVerification: 'required', // biometric or PIN, not just presence
},
excludeCredentials: existing, // stop duplicate registrations on one device
},
});
// Server side, the non-negotiables: verify the challenge is one you issued and
// unused, the origin matches exactly, the RP ID hash matches, the user-verified
// flag is set, and the signature counter has not gone backwards.Recovery is the whole project
The authentication code is a week. Recovery is the rest of it, and it is where the security property is usually thrown away. If a lost device leads to a support agent emailing a reset link, your phishing-resistant system has a password-reset-shaped hole and an attacker will use the hole rather than the front door.
- Require at least two credentials before letting a user disable password fallback. One passkey is a lockout waiting to happen.
- In workforce contexts, recover through the identity provider with verified identity proofing — never through an email link alone.
- Show users a real device list with names and last-used timestamps, and make revocation obvious and immediate.
- Log registration and removal as security events. Silent enrolment of a new authenticator is a persistence technique.
- Keep the legacy path until adoption is high, then remove it deliberately — while it exists, it is your actual security level.
A phishing-resistant login with an email-based recovery flow is an email-based login with extra steps.
The adoption surprise, in every rollout I have been part of, is that users like it. Sign-in becomes a fingerprint instead of a password manager search followed by a code from a phone. The support burden goes down rather than up, because password resets are the largest category of authentication tickets and passkeys remove most of them. The engineering is a fortnight; the policy and recovery design is the part to start early.