Your server must generate a random challenge, then verify the response: correct ceremony type, matching challenge, expected origin, the right RP ID hash, user presence and verification flags, a supported algorithm, and a credential ID not already in use. Then store the public key, not a secret. Most libraries do this, but you should know the list.
The two halves of the ceremony
WebAuthn registration has a browser half and a server half. In the browser, your page calls navigator.credentials.create() with options your server provided. The user approves with a fingerprint, face scan, PIN or security key, and the authenticator creates a new key pair. The browser returns the public key and some signed data. Your server then has to verify the result.
The W3C WebAuthn Level 3 Recommendation, dated 25 August 2026, spells out the server steps in its section on registering a new credential.
Step 1: make a good challenge
The challenge must be randomly generated by the relying party in an environment it trusts, such as the server. The spec says to store it temporarily until the operation is complete, rather than relying on the client to hold it. Challenges should be at least 16 bytes long and should only be valid for a duration similar to the ceremony timeout.
Step 2: the checks
| Check | What it proves |
|---|---|
| clientDataJSON type is webauthn.create | The data is from a registration, not another operation |
| challenge equals the one you issued | The response is fresh and belongs to your request |
| origin is one you expect | The page was really your site |
| rpIdHash is the SHA-256 of your RP ID | The credential is scoped to your domain |
| UP flag set (unless conditional creation) | A user was present |
| UV flag set if you require verification | The user was verified, not just present |
| alg matches one you offered | The key type is one you can verify |
| attestation statement valid, if you use it | The authenticator model, if you care |
| credential ID not already registered | No clash with another account |
The spec also advises rejecting credential IDs longer than 1023 bytes.
Step 3: what to store
The credential record in the spec includes the credential ID, the public key, a signature counter, the UV initialised value, transports, and the backup eligible and backup state flags. You also tie it to a user account through a user handle. Storing a user friendly nickname, as the spec suggests, helps people manage their credentials later.
Notice what you do not store: any secret. The private key never leaves the authenticator.
Do you need attestation?
Attestation tells you which authenticator model created the key. Many consumer sites do not need it and accept "none". The spec says if no attestation is given, you must decide whether that is acceptable under your policy. Ask for it only when you have a reason, such as a regulated environment that limits which devices can be enrolled.
Common mistakes
- Reusing a challenge or generating it in the browser.
- Accepting any origin, or comparing with a loose "contains" check.
- Not checking the RP ID hash.
- Using a short or predictable user handle. It should not contain personal data.
- Letting a user register a first credential without an existing session or verified recovery.
Quick check
Do I need to write this verification code myself?
Usually no. Use a maintained WebAuthn library, and test that it enforces each check above. If the library cannot be configured to check origin and RP ID, pick another.
Is the public key sensitive?
It is not a secret. Its value to an attacker is low, but still store it with the account record and protect integrity.
Safeguard's SBOM and dependency scanning shows which WebAuthn and auth libraries you ship, so you can watch their advisories. A free scan is available.
Sources
Last reviewed: 19 October 2026.