What Are Passkeys?
A fundamentally different approach
Passwords, TOTP, recovery codes, and magic links all share one property: the secret is known to both the server and the user (or the user’s device). If the server’s database is breached, secrets are exposed.
Passkeys are different. They use public-key cryptography: the user’s device generates a key pair (public key + private key). The server stores only the public key. The private key never leaves the device.
Authentication works by challenge-response: the server sends a random challenge, the device signs it with the private key, the server verifies the signature with the public key. The private key is never transmitted — not at registration, not at login, not ever.
Why passkeys are phishing-resistant
When a passkey is created, it is bound to a specific origin (domain). A passkey created for yourapp.com will only respond to authentication requests from yourapp.com.
If an attacker creates a phishing page at evil.com that looks identical to your login page, the browser will not use the passkey because the origin does not match. The user cannot be tricked into using their passkey on the wrong site, even if the page looks exactly right.
This is enforced by the browser, not by the user. The user does not need to check the URL — the browser does it automatically.
How it works at a high level
Registration (one-time setup):
- The server generates a random challenge
- The browser prompts the user to create a credential (fingerprint scan, Face ID, or PIN)
- The device generates a key pair and returns the public key + credential ID
- The server stores the public key and credential ID
Authentication (every login):
- The server generates a random challenge and sends it with the stored credential IDs
- The browser finds the matching credential on the device
- The user authenticates locally (fingerprint, face, PIN)
- The device signs the challenge with the private key
- The server verifies the signature with the stored public key
- If valid, the server creates a session
The local authentication (step 3) is what makes passkeys “something you have” (the device) combined with “something you are” (biometric) or “something you know” (PIN). But the biometric or PIN never leaves the device — only the cryptographic signature is sent to the server.
Platform vs. roaming authenticators
Platform authenticators are built into the device: Touch ID on Mac, Windows Hello, fingerprint on Android. The passkey is stored on that specific device.
Roaming authenticators are external hardware keys: YubiKey, Titan Security Key. They connect via USB, NFC, or Bluetooth and work across devices.
Modern passkeys can also sync across devices via cloud accounts (iCloud Keychain, Google Password Manager). A passkey created on your iPhone syncs to your Mac automatically. This solves the “lost device” problem that plagued earlier WebAuthn implementations.
The WebAuthn API
WebAuthn (Web Authentication API) is the browser API that implements passkeys. It provides two methods:
navigator.credentials.create()— registration (creates a new credential)navigator.credentials.get()— authentication (signs a challenge with an existing credential)
On the server side, we use the @simplewebauthn/server library to generate challenges and verify responses. The next two lessons implement both flows.
Exercises
Exercise 1: Check if your browser supports WebAuthn: open the browser console and type typeof navigator.credentials.create. If it returns "function", your browser supports it.
Exercise 2: Research which authenticators your devices support. Does your laptop have a fingerprint reader or Face ID? Does your phone support passkeys?
Why is the private key never sent to the server?