The Webauthn registration ceremony, step by step

Registration is where a user hands you a public key. Everything you will later rely on depends on doing these six steps properly.

The six steps

  1. Your server generates creation options: a random challenge, the relying party identity, the user handle, and your policy.
  2. The browser passes them to the authenticator.
  3. The authenticator creates a key pair, keeps the private half, and signs an attestation object.
  4. The browser returns that object to your server.
  5. Your server verifies it: the challenge, the origin, the relying party hash, the flags, the signature.
  6. You store the credential identifier, the public key, and the sign counter.

The challenge must be yours, once

Generate it server side, at least sixteen random bytes, store it against the session, and delete it after one use. A challenge that can be replayed is not a challenge.

The mistake to avoid: generating it in JavaScript. The whole ceremony rests on the server having chosen a value the client could not predict.

The user handle is not the username

The user handle identifies the account to the authenticator. It ends up stored on the device, sometimes shown in a system dialogue, and it is not the place for an email address or anything else you would rather not see there.

Use an opaque, stable identifier, sixty-four bytes at most. A random value mapped to your account in your own database is the right answer.

The origin check is the anti-phishing property

The browser records the origin it was on and the authenticator signs over it. Your server checks it matches. That single check is what makes Webauthn phishing-resistant, and it is why a lookalike domain cannot obtain a usable response.

Related: version 5.3.4 of the framework fixed a case where cross-origin responses were accepted when no top-origin validator was configured. The safe default is to refuse, and it now does.

In Symfony

composer require web-auth/webauthn-framework

The bundle exposes the routes and the ceremony, and a Stimulus component handles the browser side. The documentation covers the configuration; the next article covers what happens when the user comes back.