What are passkeys? A developer's guide
What are passkeys? A developer's guide to WebAuthn public-key credentials: how registration and sign-in work, synced vs device-bound keys, and what to store.
A passkey is a public-key credential that replaces a password. When a user creates one, their device generates a key pair: the private key stays on the device (or in a synced password manager), and your server stores only the public key. To sign in, the device proves it holds the private key by signing a fresh challenge from your server, after the user unlocks it with Face ID, Touch ID, Windows Hello, a screen lock PIN or a security key. Nothing secret ever crosses the network, and the credential only works on the website it was created for.
Passkeys are built on the W3C Web Authentication standard (WebAuthn) and the FIDO Alliance’s CTAP protocol. “Passkey” is the consumer-friendly name for what the specs call a discoverable FIDO credential.
The key pair: what lives where
| Piece | Where it lives | Secret? |
|---|---|---|
| Private key | The authenticator: secure hardware on the device, a security key, or a passkey provider like iCloud Keychain, Google Password Manager or 1Password | Yes, never leaves the authenticator |
| Public key | Your server | No, safe to store and even to leak |
| Credential ID | Your server and the authenticator | No, it’s an identifier |
| User handle | Your server and the authenticator | No, an opaque ID you choose (not an email) |
Because your database holds only public keys, a database breach doesn’t hand attackers anything they can sign in with. There’s no hash to crack and no credential to stuff into another site.
How registration works
Registration (WebAuthn calls it “attestation”, though most apps don’t verify attestation) creates the credential:
- Your server generates a random challenge and sends it to the browser along with your relying party ID (RP ID, usually your registrable domain such as
example.com) and a user handle. - The browser calls
navigator.credentials.create(). - The authenticator asks the user to verify (biometric or PIN), generates a new key pair scoped to that RP ID, and returns the public key, a credential ID and a signed bundle of data.
- The browser adds
clientDataJSON, which includes the challenge and the origin the request came from. - Your server checks the challenge, the origin and the RP ID hash, then stores the public key and credential ID against the user.
const credential = await navigator.credentials.create({
publicKey: {
challenge, // random bytes from your server
rp: { id: "example.com", name: "Example" },
user: { id: userHandle, name: "ada@example.com", displayName: "Ada" },
pubKeyCredParams: [
{ type: "public-key", alg: -8 }, // EdDSA
{ type: "public-key", alg: -7 }, // ES256
{ type: "public-key", alg: -257 } // RS256
],
authenticatorSelection: {
residentKey: "required", // discoverable credential = passkey
userVerification: "required"
}
}
});
How sign-in works
Sign-in (WebAuthn calls it an “assertion”) is the same idea in reverse:
- Your server sends a fresh random challenge.
- The browser calls
navigator.credentials.get(). Because passkeys are discoverable, you don’t need to know who the user is first. The device can show a list of passkeys it holds for your RP ID. - The user picks one and unlocks it.
- The authenticator signs the challenge together with authenticator data (RP ID hash, flags, a signature counter). The browser adds
clientDataJSONwith the origin. - Your server looks up the public key by credential ID, verifies the signature, checks the challenge matches and is unused, checks the origin and RP ID, and confirms the user-verification flag is set.
If every check passes, the user is who the public key says they are.
Synced vs device-bound passkeys
There are two kinds of passkeys in the wild:
- Synced passkeys live in a passkey provider and are end-to-end encrypted and synced across a user’s devices. Create one on an iPhone and it appears on their Mac. iCloud Keychain, Google Password Manager and 1Password all work this way. This is what most consumers will use, and it largely solves the “I got a new phone” problem.
- Device-bound passkeys never leave a single piece of hardware, such as a YubiKey or some platform authenticators. They offer the strongest guarantee that a credential exists in exactly one place, which matters in high-assurance settings, but losing the device means losing the credential.
Users can also sign in on a computer with a passkey stored on their phone using the cross-device flow: the computer shows a QR code, the phone scans it, and the two devices confirm they’re physically near each other over Bluetooth before the phone signs.
The RP ID: why passkeys are scoped to a domain
Every passkey is bound to the RP ID it was created for. The browser refuses to use a credential for any origin that doesn’t match that domain. This is what makes passkeys resistant to phishing, which we cover in detail in why passkeys stop phishing.
It also has a practical consequence: if you change the domain your sign-in runs on, existing passkeys won’t work there. Pick your sign-in domain carefully and early.
What your server needs to get right
Implementing WebAuthn yourself is very doable, but the checklist is longer than it looks:
- Generate challenges with a cryptographically secure random source, store them server-side, and make each one single use with a short expiry.
- Verify the origin and RP ID on every ceremony, not only the signature.
- Require user verification if you’re treating the passkey as the only factor.
- Handle multiple algorithms (EdDSA, ES256, RS256) and COSE key parsing.
- Track the signature counter. If an authenticator that reports a counter ever goes backwards, the credential may have been cloned. Note that many synced passkeys always report zero, so only a regression from a non-zero value is meaningful.
- Decide how users recover when they lose every device. See passkey account recovery.
Passkeys and your existing auth
Passkeys handle the “prove you’re you” step. Your app still needs sessions, tokens and probably single sign-on across apps. That’s the job OpenID Connect already does well, which is why a hosted passkey provider that speaks OIDC can slot into an existing stack without new client code. For the comparison against older methods, see passkeys vs passwords, SMS codes and magic links.
Using passkeys with Gatekeep
Gatekeep runs the WebAuthn ceremonies for you on a hosted sign-in page, then hands your app a signed ID token over standard OpenID Connect. It handles challenges and origin checks, revokes credentials whose sign count regresses, and supports synced passkeys from iCloud Keychain, Google Password Manager and 1Password as well as security keys.
Read how sign-in works, try the quickstart, and request early access.