Password Password

Security, explained for the people who have to fix it.

Passwords and Auth

Passkeys explained for people who run a login page

What a passkey is in WebAuthn terms, why origin binding makes it phishing-resistant, synced versus device-bound, and what changes on the server side.

A passkey is a public-key credential that a browser and an authenticator create together for one specific website, and that only that website can ever ask to use. That one sentence carries the whole security argument, and this article unpacks it for the person who has to change the login page: what the pieces are called in the specification, why a passkey cannot be phished the way a password or a one-time code can, what "synced" means and what it costs, and what your server now has to store and check.

The vocabulary, from the specification

The W3C Web Authentication specification (Level 3, a W3C Recommendation dated 25 August 2026) defines the actors. The Relying Party is your site: the thing that asks for a credential to be created and later verifies a signature. The authenticator is the device or software that holds the private key and signs. It can be the platform (a phone's secure enclave, a laptop's TPM), a roaming hardware key, or a password manager that implements the protocol.

Registration is a ceremony: the Relying Party sends a challenge, the authenticator generates a new key pair for this site, stores the private key, and returns the public key with an attestation statement about what kind of authenticator made it. Authentication is a second ceremony: the Relying Party sends a fresh challenge, the authenticator signs it with the private key, and the returned assertion proves "that the user controls the credential private key of a previously-registered public key credential". The private key never leaves the authenticator in either ceremony.

The FIDO Alliance's plain-language definition is consistent with this: a passkey is "an authentication credential based on FIDO standards, that can be stored on your phone or computer, or in a hardware security key", used "with the same process that they use to unlock their device (biometrics, PIN, or pattern)". The biometric, where one is used, is checked locally and, in the Alliance's words, "is never sent to any remote server". Your server never sees a fingerprint; it sees a signature.

Why the phishing argument holds

Every credential is scoped to a Relying Party ID, which is a domain. The specification says a credential "can only be accessed by origins belonging to that Relying Party", and that "this scoping is enforced jointly by conforming User Agents and authenticators". The RP ID becomes part of the credential at creation and "determines where it can be used".

Follow what that does to a phishing page. An attacker registers a look-alike domain and builds a perfect copy of your login form. A user who types a password into it has given the password away; a user who types a six-digit code from an authenticator app has given that away too, and a relay in the middle can forward both to the real site within the code's validity window. With a passkey, the look-alike page calls the browser's WebAuthn API, the browser looks up credentials for the look-alike's origin, finds none for your site's RP ID, and the ceremony fails. The user cannot be talked into approving the wrong site, because the browser will not offer the credential. Nothing in the user's judgment is on the critical path.

NIST SP 800-63B, in Section 3.2.5, defines phishing resistance as "the ability of the authentication protocol to prevent the disclosure of authentication secrets ... to an impostor verifier", achieved through channel binding or verifier name binding, and states plainly that "authenticators that involve the manual entry of an authenticator output (e.g., out-of-band and OTP authenticators) SHALL NOT be considered phishing-resistant". Table 1 of the same document makes a phishing-resistant option "Recommended; Must be available" at AAL2 and "Required" at AAL3. WebAuthn's origin scoping is verifier name binding.

Synced versus device-bound

The FIDO Alliance distinguishes "passkeys that are synced between user's devices via a cloud service", called synced passkeys, from "those that never leave a single device (including those on FIDO security keys)", called device-bound passkeys.

Synced passkeys are what made the technology usable for consumers: lose the phone, sign in on the new one, the credential is there. The Alliance states that "passkey syncing is end-to-end encrypted, and passkey providers have strong account security protections". The trade you should understand as a Relying Party is that the security of a synced passkey now includes the security of the sync account. That is still far better than a password, because the private key still cannot be phished from the user, but it is not the same assurance as a key that physically cannot be copied. If you need to know which kind you were given, attestation at registration can tell you, at the cost of more complexity and more reasons for a registration to fail; most consumer sites do not ask.

Discoverable credentials (the specification's term; older documents say resident keys) are the ones that make a username field optional: they are "usable in authentication ceremonies where the Relying Party does not provide any credential IDs". The browser shows the user which accounts have passkeys for this site and the user picks one. That is the "sign in with a passkey" button with no name typed first.

What changes on your server

Less than you might fear, and it is all additive.

  • Storage. Per credential: the credential ID, the public key, the signature counter (the specification includes one for clone detection; treat a counter that goes backwards as a signal, not proof), the RP ID it was created for, and a user-visible label and creation date. A user may have several; design for a list.
  • Challenges. A fresh random challenge per ceremony, stored server-side, checked once, and expired. Reusing a challenge is the mistake that turns a signature into a replayable token.
  • Verification. On authentication, check the origin in the signed client data matches your origin exactly, check the RP ID hash, check the challenge, verify the signature against the stored public key, and update the counter. Use a maintained library for your platform; the byte-level parsing is not the place to be original.
  • Recovery. The hard part is not cryptography, it is what happens when a user has no authenticator left. A recovery path that is weaker than the passkey (an emailed link, a phone code) becomes the real security level of the account. NIST's authenticator-binding guidance and your own risk appetite have to answer this; the honest options are a second registered passkey, a set of one-time recovery codes generated at enrolment, or an identity-proofing step with a delay.
  • Coexistence. Keep the password path during migration but make the passkey the default when one exists, and watch the share of logins on each path. Sites that offer passkeys as an optional extra and never promote them keep all of their phishing exposure.

A registration flow in outline

Here is the shape of the two server endpoints, with the library call left abstract because it differs by language.

def begin_registration(user):                         # registration step 1: server issues options
    challenge = secrets.token_bytes(32)
    store_challenge(user.id, challenge, ttl_seconds=120)
    return {
        "rp": {"id": "example.org", "name": "Example"},
        "user": {"id": user.opaque_id, "name": user.email, "displayName": user.name},
        "challenge": b64url(challenge),
        "pubKeyCredParams": [{"type": "public-key", "alg": -7}, {"type": "public-key", "alg": -257}],
        "authenticatorSelection": {"residentKey": "preferred", "userVerification": "preferred"},
        "excludeCredentials": [{"type": "public-key", "id": c.id} for c in user.credentials],
    }

def finish_registration(user, response):              # registration step 2: browser returns the attestation
    challenge = pop_challenge(user.id)                # single use, or the ceremony is replayable
    cred = webauthn_lib.verify_registration(response, expected_challenge=challenge,
                                            expected_origin="https://example.org", expected_rp_id="example.org")
    user.credentials.append(Credential(id=cred.id, public_key=cred.public_key, sign_count=cred.sign_count,
                                       created=now(), label="new passkey"))

The excludeCredentials list stops the same authenticator registering twice; residentKey: preferred asks for a discoverable credential where the authenticator supports it, so the usernameless flow works later.

What to do

  • Add passkeys as a first-class login method with a maintained WebAuthn library, single-use server-side challenges, exact origin checking, and a per-user list of credentials with labels the user can name and remove.
  • Decide your recovery path before launch and make sure it is not weaker than the thing it recovers.
  • Promote the passkey path once a user has one; log the share of logins by method so you can see the password path shrink.
  • Only require attestation or device-bound keys if a regulator or your threat model demands it; for most sites, synced passkeys with no attestation is the right first step.
  • Keep MFA on the password path for users who have not migrated, and prefer the number-matching or phishing-resistant forms, for the reasons the MFA fatigue article on this site covers.

The limitation to state: passkeys remove phishing of the credential, not every account takeover. A session cookie stolen after a successful passkey login is still a valid session, and a recovery flow that accepts an emailed link is still an emailed link. Passkeys close the largest door; the smaller ones are separate articles.

Everything on this site is written for defenders and for people securing systems they own or are authorised to test. It describes how attacks work so they can be recognised and closed; it is not a guide to accessing anyone else's systems.

Sources

  1. W3C, "Web Authentication: An API for accessing Public Key Credentials - Level 3", W3C Recommendation (25 August 2026)
  2. FIDO Alliance, "Passkeys" (definitions, synced vs device-bound)
  3. NIST, SP 800-63B "Digital Identity Guidelines: Authentication and Authenticator Management" (Section 3.2.5, phishing resistance; Table 1)