Published:

Tagged: Passkeys WebAuthn Security Authentication

Passkeys from first principles

Passwords are the oldest unfixed bug in software. We have spent thirty years adding layers around them and moving them about, and passkeys are the first widely deployed change that removes the shared secret rather than relocating it. This is a tour of where passkeys sit in the authentication landscape, and how the two ceremonies behind them work; a follow-up post builds them from scratch in Ruby on Rails.

The password has a structural problem that no amount of policy can patch. It is a shared secret: you know it, and the server knows it. Every shared secret is something that can be guessed, reused across sites, leaked in a breach, or handed to the wrong person by a convincing email. We have spent decades building defences around that one weakness, and most of them treat the symptom rather than the cause.

Passkeys are different because they stop sharing the secret at all. The aim of this post is to make that idea concrete: first by placing passkeys properly within the authentication ecosystem, and then by walking through the two ceremonies that make them work.


The authentication ladder

It helps to see passkeys as the next step on an authentication ladder we have been climbing for years, because each step was an attempt to fix the step below it:

A staircase of five authentication methods rising by year. Each step shows where its reusable secret lives and whether it can be phished: passwords (1961, the server database, phishable); passwords plus a one-time code (1986, database and device, phishable); federated identity (late 2000s, the identity provider, phishable); magic links (mid-2010s, the email inbox, phishable); and passkeys (2022, no shared secret, unphishable).

1961: Passwords alone

A single shared secret, an idea that goes back to MIT’s CTSS in 19611. If it is guessed, reused, or phished, the account is gone. Every other step exists because this one leaks.

1986: Passwords plus a one-time code

Hardware tokens such as RSA’s SecurID landed in 19862, and TOTP apps3 and SMS codes4 later carried the idea into everyday use, adding a second factor: something you have, on top of something you know. This genuinely raises the cost of an attack, yet it does not close the most common door. A user who can be tricked into typing their password into a lookalike site can just as easily be tricked into typing the six-digit code straight after it. The phishing page simply relays both to the real site in real time. SMS adds its own weakness on top, since numbers can be ported away from their owner.

Late 2000s: Federated identity

The “Sign in with Google” button lets users authenticate through a provider such as Google, Apple, or GitHub, which genuinely removes the password from your application: you never store one and never check one, because you delegate the whole question to an identity provider. Strictly this is OpenID Connect5, the authentication layer built on top of OAuth 2.06, the framework finalised in 2012, even though almost everyone calls the bundle OAuth. The catch is that it removes the password from your app without removing the shared secret from the system: it centralises it. The user still proves themselves to the provider, and usually still does so with a password, so the phishing target moves to the provider’s login page rather than disappearing. Every account a user federates then rests on the security of that one provider account, the provider gets to see everywhere its users sign in, and it becomes a single point of lockout.

Popularised by the SaaS boom of the mid-2010s, sending a sign-in link by email removes the password from the user’s head, which is a real improvement, but it makes the same move as federated identity, relocating the secret rather than removing it. The link is now a bearer token sitting in an inbox, and that inbox is itself usually protected by a password. It is also exposed on the way there: the link crosses mail servers and relays you do not control, over connections where TLS (the encryption that protects data in transit, the “S” in HTTPS) is only opportunistic between mail servers and can be stripped, so a token that grants account access spends its first moments on infrastructure outside anyone’s security guarantees. The security of the account collapses back onto the security of the mailbox.

2022: Passkeys

Instead of proving you know a secret, you prove you hold a private key, and you never reveal that key to anyone. The server keeps only the matching public half. There is nothing in the database for an attacker to steal and replay, and, as we will see, the browser refuses to use the key on the wrong site, so phishing stops working by design.


Two patterns show up at every step of the authentication ladder:

  • The first is phishability: almost every step except the last can be defeated by a user who is fooled into cooperating with the attacker, and passkeys are the first step where cooperation is not enough, because the user is no longer the one checking that the site is genuine.
  • The second is more fundamental: every step except passkeys keeps a shared secret somewhere, whether in the user’s head, in an inbox, or in a provider’s database, and the entire history of the ladder is really the story of moving that secret to ever better-defended locations.

Federated identity is the high point of that strategy rather than an exception to it: it does not abolish the secret, it hands it to a provider who can guard it better than you can. Passkeys are the first widely deployed step that stops there being a shared secret to move at all.


What a passkey actually is

Strip away the branding and a passkey is a public/private key pair created for one website.

When you register, your device generates a fresh key pair. The private key is written into a protected store: the secure enclave on a phone, a TPM on a laptop, or the chip inside a hardware security key. It is designed never to leave that store in usable form. The public key, which is useless on its own, is sent to the website and saved against your account.

From then on, signing in means the website sends a random challenge, your device signs it with the private key, and the website checks the signature against the public key it stored. Possession of the private key is proven without the key ever crossing the wire.

A two-phase diagram of a passkey key pair. Registration: your device generates a key pair; the private key stays locked in the device's secure store and never leaves, while only the public key travels across the network and is saved against your account on the website. Signing in: the website sends a random challenge, the device unlocks with a biometric or PIN and signs the challenge with the private key inside the secure store, the signature travels back, and the website verifies it against the stored public key. The private key never crosses the wire.

Two consequences fall straight out of this design, and they are the whole reason passkeys matter:

Naming the standards correctly

The terminology around passkeys is a small thicket, and getting it right makes the rest of the field readable.

The taxonomy that trips people up

Three distinctions cause most of the confusion when teams first adopt passkeys.

Platform versus roaming authenticators. A platform authenticator is built into the device you are using: Touch ID on a Mac, Windows Hello, the secure enclave on a phone. A roaming authenticator is a separate object you carry between devices, almost always a hardware security key such as a YubiKey. Your app does not usually need to care which is which, but the choice shapes the user’s recovery story.

You might expect a platform authenticator to trap a passkey on one machine, since the private key is generated inside that device’s secure hardware. In practice it usually does not, because the operating system’s credential manager copies the passkey between all the devices signed into the same account. Create a passkey with Touch ID on a Mac that shares an iCloud account with your iPhone, and iCloud Keychain syncs it to the phone, so you can sign in there without registering separately; Google Password Manager does the same across your Android and Chrome devices. The private key still never leaves secure storage on each device; the credential manager moves an encrypted copy, end-to-end encrypted so Apple or Google cannot read it. Whether a passkey travels this way is the synced versus device-bound distinction, next.

Synced versus device-bound. This is the distinction that made passkeys usable for ordinary people. A device-bound passkey lives and dies on one piece of hardware: lose the device and you lose the key. A synced passkey is backed up and shared across a user’s devices by a credential manager such as iCloud Keychain or Google Password Manager, so a new phone arrives already holding the user’s passkeys. Synced passkeys trade a little theoretical hardware-level assurance for an enormous gain in practicality, and they are why passkeys finally crossed into the mainstream9. One side effect is that synced passkeys often report a signature counter of zero, a detail that matters when we verify signature counters in part two.

Discoverable versus non-discoverable credentials. A non-discoverable credential, historically called a non-resident key, stores nothing on the authenticator itself. The server has to remind the authenticator which credentials exist by sending a list, which means the user has to identify themselves first, typically by typing a username. A discoverable credential, the resident key, stores enough on the authenticator that it can present the user’s accounts without being told who they are in advance. Discoverable credentials are what make truly usernameless, passwordless sign-in possible: the user lands on the page, taps a button, picks an account, and authenticates. That is the experience this post builds.

Why passkeys cannot be phished

This is the part worth slowing down on, because it is where passkeys earn their reputation.

When a credential is created, it is bound to a Relying Party ID, the RP ID, which is essentially your site’s domain. When the user later tries to authenticate, the browser will only offer credentials whose RP ID matches the origin of the page actually being viewed. A passkey created for pardel.dev is invisible on parde1.dev, and the user is never asked to make that judgement.

That last clause is the crucial one. With passwords, the human is the component checking that the site is genuine, and humans are not reliable at spotting a swapped letter in a domain under time pressure. With passkeys, the browser performs the check, mechanically, every time, using the real origin rather than the one the user thinks they are on. The signed payload that comes back to your server includes the origin the browser saw and the exact challenge you issued, so even a man-in-the-middle relaying messages in real time cannot produce a valid signature for your domain. The thing that defeats classic phishing is moved out of the user’s hands entirely.


The two ceremonies

Everything in WebAuthn reduces to two flows, and they are pleasingly symmetrical. The specification calls them ceremonies:

Both follow the same shape: the server issues a random challenge, the authenticator produces a signature over it, and the server verifies that signature. Hold that round-trip in your head and the code below is mostly plumbing.

Registration

  1. The browser asks the server to start registration.
  2. The server generates a random challenge, remembers it, and returns the creation options.
  3. The browser calls navigator.credentials.create(), which prompts the user for a biometric or PIN and generates the key pair.
  4. The browser sends the new public key and a signature back to the server.
  5. The server verifies the response against the stored challenge and saves the public key against the account.

Sequence diagram of the registration ceremony between the browser and the server. Step 1: the browser asks the server to start registration. Step 2: the server generates and remembers a random challenge and returns the creation options. Step 3: on the browser, navigator.credentials.create() prompts the user for a biometric or PIN and generates the key pair, keeping the private key on the device. Step 4: the browser sends the new public key and a signature to the server. Step 5: the server verifies the response against the stored challenge and saves the public key against the account.

Authentication

  1. The browser asks the server to start sign-in.
  2. The server generates a fresh challenge, remembers it, and returns the request options.
  3. The browser calls navigator.credentials.get(), the user picks a passkey and confirms with a gesture.
  4. The browser sends a signature over the challenge back to the server.
  5. The server looks up the stored public key, verifies the signature, and opens a session.

Sequence diagram of the authentication ceremony between the browser and the server. Step 1: the browser asks the server to start sign-in. Step 2: the server generates and remembers a fresh challenge and returns the request options. Step 3: on the browser, navigator.credentials.get() lets the user pick a passkey and confirm with a gesture, signing the challenge with the private key. Step 4: the browser sends a signature over the challenge to the server. Step 5: the server looks up the stored public key, verifies the signature, and opens a session.


What’s next: building it in Rails

The two ceremonies above are the entire protocol: a challenge out, a signature back, a verification. Everything else is plumbing. In the follow-up post, Building passkeys in Ruby on Rails from scratch, we turn these diagrams into a working passwordless, usernameless login: the server-side options and verification with the webauthn-ruby gem, the browser calls, the parts a from-scratch demo tends to skip, and a small gem that packages the whole thing.


References

  1. Professor Emeritus Fernando Corbató, MIT computing pioneer, dies at 93, MIT News (the CTSS password, 1961).
  2. RSA SecurID, RSA (the SecurID one-time-password hardware token).
  3. RFC 6238: TOTP: Time-Based One-Time Password Algorithm, IETF (2011).
  4. NIST SP 800-63B: Digital Identity Guidelines — Authentication and Lifecycle Management, NIST (2017; §5.1.3.3 restricts SMS one-time codes).
  5. OpenID Connect Core 1.0, OpenID Foundation.
  6. RFC 6749: The OAuth 2.0 Authorization Framework, IETF (2012).
  7. Web Authentication (WebAuthn) Level 2, W3C Recommendation.
  8. Passkeys, FIDO Alliance.
  9. Apple, Google and Microsoft commit to expanded support for FIDO standard, Apple Newsroom, May 2022 (passkeys).