Adam Innes · Blog

WebAuthn Is a W3C Standard. Passwords Get a Real Rival

· 3 min · security, webauthn, authentication, web standards

On March 4 the W3C and the FIDO Alliance announced that Web Authentication is now an official web standard. WebAuthn Level 1 is a full W3C Recommendation, and according to that announcement it already works in Windows 10, Android, Chrome, Firefox and Edge, with Safari support in preview. After years of “passwords are dead” talks, the replacement is finally a finished standard that ships in mainstream browsers.

What an authenticator actually does

The Level 1 Recommendation describes an API for creating and using public key credentials. The thing holding the keys is called an authenticator. It can be built into your device (a platform authenticator, which the spec says can live in something like a TPM or secure element) or it can be a roaming authenticator you carry around and connect over USB, Bluetooth or NFC, like a security key.

There are two ceremonies. Registration runs through navigator.credentials.create(). Your server sends a random challenge, the authenticator makes a brand new key pair for your site, keeps the private key, and hands back the public key and a credential ID for you to store against the user’s account. Authentication runs through navigator.credentials.get(). The server sends a fresh challenge, the user touches the key or scans a finger, and the authenticator signs the challenge. The server checks that signature with the public key it saved.

Here’s roughly what registration looks like in the browser. The challenge and user handle come from your server as byte arrays.

const credential = await navigator.credentials.create({
  publicKey: {
    challenge,                                   // random bytes from your server
    rp: { name: "Example" },                     // id defaults to this page's domain
    user: { id: userHandle, name: "adam@example.com", displayName: "Adam" },
    pubKeyCredParams: [
      { type: "public-key", alg: -7 },   // ES256
      { type: "public-key", alg: -257 }  // RS256, for authenticators that only do RSA
    ]
  }
});
// base64url encode credential.rawId and the response buffers, then send them to your server

Why phishing stops working

Every credential is scoped. The spec ties each one to a relying party ID, which defaults to the domain of the page that created it, and the authenticator will only use a credential for that same ID. On top of that, the browser (not the user, and not the page) fills in the origin of the page making the request, and that origin ends up inside the data the authenticator signs.

A lookalike domain can’t ask your security key for a signature for the real site, because the browser reports the lookalike’s own domain. If an attacker relays your real site’s challenge through their page, the signed data still carries the attacker’s origin, and the real server’s origin check fails. There’s no code for the user to type into the wrong box, which is exactly the weakness of SMS codes. And since the server only stores public keys, a database breach gives an attacker nothing they can log in with. Keys are also unique per site, so they can’t be used to track someone across sites.

What your site needs to add

First, HTTPS. Browsers only expose the API in secure contexts. Then you need two server endpoints per ceremony: one that creates the options and a challenge, and one that verifies the response.

The challenge must be random and generated on the server, and the spec says it should be at least 16 bytes. Store it until the ceremony finishes so you’re not trusting the client to echo it honestly. Section 7, WebAuthn Relying Party Operations, is the checklist for verification. You confirm the client data type, that the challenge matches, that the origin is yours, that the RP ID hash is the one you expect, that the user present flag is set, and, when a user signs in, that the signature verifies against the public key you stored. If an authenticator’s signature counter is in use and ever fails to increase, the spec treats that as a hint the authenticator may have been cloned.

A few design choices are worth getting right on day one. The user handle (user.id) shouldn’t contain an email address or username. The spec recommends 64 random bytes stored on the account. Attestation, which proves what model of authenticator made the key, defaults to "none", and I’d leave it there unless you really need to restrict hardware. Plan for lost keys too. The spec is clear that a private key generally never leaves the authenticator that created it, so losing that device means losing the credential. It recommends letting people register several authenticators on one account, say a laptop’s built in one plus a security key in a drawer.

My advice is to use a maintained server library for the CBOR parsing and signature checks, and spend your own time on the account flows around it, like adding, naming and removing keys and recovery.

Where this leaves passwords

Passwords aren’t going away this year, and I expect most sites to start by offering WebAuthn as a second factor. That’s still a big step up. It’s one of very few login methods where the browser itself refuses to hand a credential to the wrong site, and now that it’s a Recommendation there’s no reason to wait on the spec.

← all posts