Passkey registration fails
Origin and RP-ID pinning, the allowed-origin list, counter regression and cross-origin refusals — every passkey ceremony error verbatim, with what each one means.
A passkey is a key pair held by your device or security key: the private half never leaves the hardware, and the browser will only use it on the exact website it was created for. That last property is why passkeys resist phishing, and it is also why almost every registration failure in practice is an address problem rather than a hardware problem. This article gives you every error the ceremony can produce, verbatim, and tells you which are configuration, which are the browser protecting the user, and which are a wiring gap on our side that you cannot fix.
What a ceremony is, and where it can fail
Registration and sign-in each happen in two calls, called a ceremony: the first issues a random challenge, the second returns what the authenticator signed. Nothing is taken on trust from the browser — the credential identifier and the public key are read out of the signed data, so a caller cannot register a public key it does not hold.
POST /api/v1/auth/webauthn/register/begin (signed in)
POST /api/v1/auth/webauthn/register/finish (signed in)
POST /api/v1/auth/webauthn/assert/begin (signed in — step-up)
POST /api/v1/auth/webauthn/assert/finish (signed in — step-up)
POST /api/v1/auth/webauthn/login/begin (public — passkey as a first factor)
POST /api/v1/auth/webauthn/login/finish (public)
GET/PATCH/DELETE /api/v1/auth/webauthn/credentials[/{id}]The two login routes are public because a passkey used as a primary factor is presented before any session exists. They name no user and return an empty credential list, so they cannot be used to find out which addresses have accounts.
The options we ask the browser for matter because they make some hardware refuse outright: a discoverable (resident) credential is required, the accepted algorithms are ES256, EdDSA and RS256 in that order, user verification is preferred rather than required by default, and the ceremony times out after 60 seconds. Requiring a discoverable credential is deliberate — a non-discoverable one cannot be used for username-less sign-in, which is the main reason to build passkeys at all. Very old security keys with no room for resident keys fail here and cannot be made to work.
Origin: the error you are most likely to see
The origin check is the phishing resistance. It compares the address the browser reports against a fixed list, and the failure message names the list for you — which is what makes it diagnosable without asking us for logs:
webauthn: clientDataJSON origin is not an allowed origin: "https://console.example.net" (allowed: https://app.example.com)
Read the two halves: the left is where the browser was, the right is what we accept. Any difference at all — a different hostname, a www. prefix, a custom domain in front of ours, an extra port, http instead of https — is a mismatch. The list is pinned to configuration rather than to the request's Host header, so an attacker cannot talk us into accepting their own origin with a forged header.
| Situation | Symptom | Who fixes it |
|---|---|---|
| A person reached the console through an unexpected address — a reverse proxy, a vanity domain, a link with a stale hostname | The origin error names their address on the left | You. Send them to the canonical address and the ceremony works. |
| The interface is served from a different host than the API's configured base URL | Every ceremony for every user fails on origin | Us. This is the wiring gap in the callout above. Report both hostnames. |
The deployment's base URL is unparseable and fell back to localhost | Ceremonies fail everywhere, and passkeys and OIDC both advertise localhost. The startup log says so: iam: APP_BASE_URL is not a parseable URL; WebAuthn and OIDC will advertise localhost | Us. A deployment fault. |
| An empty allowed-origin list was configured | The service refuses to start rather than accepting anything: webauthn: at least one allowed origin is required | Us. This is deliberate — an empty list would accept any origin and reduce a passkey to an ordinary bearer token. |
One nuance: a localhost origin is added only when the relying-party ID is literally localhost, so a production deployment cannot pick one up by accident — which is why a developer's working setup tells you nothing about whether your production hostname is right.
RP-ID pinning, and why moving domains breaks old passkeys
The relying-party ID is the domain a credential belongs to. It is stored on each credential, and on sign-in the check is made against that stored value rather than against a single global setting. A credential created under one domain therefore cannot be asserted under another, and the refusal is:
webauthn: relying-party ID hash does not match
The practical consequence: if your Qorionix address changes, existing passkeys stop working and cannot be migrated. There is no rewrite, and there should not be — a credential that could be re-pointed at a new domain would be a credential an attacker could re-point too. Everyone re-registers. Plan for that before a domain change rather than after, and keep at least one administrator on a password plus an authenticator app until the new passkeys are in place.
Counter regression: a possible cloned authenticator
Most authenticators keep a counter that increases with every signature. If we see a counter that is the same as or lower than the last one we stored, the assertion is refused, because that is what a cloned device looks like:
webauthn: sign counter went backwards — possible cloned authenticator: authenticator reported 41, stored 57
Two honest caveats. An authenticator reporting zero on both sides is accepted, because a zero counter means the device does not implement the feature at all — so this check protects some hardware and not other hardware. And a benign cause exists: some platform authenticators synchronise a credential across a person's own devices and can present a lower counter than the one we last saw. Treat it as a possibility rather than a certainty, but do not wave it through — delete the credential, have them register a new one, and look at their recent sign-in history while you are there.
Every ceremony error, verbatim
| Message | Status | Meaning |
|---|---|---|
challenge not found, already used, or expired | 401 | Challenges are single-use and consumed on the first attempt. A retried request, a double-submitted form or a ceremony left open too long all land here. Start again from /begin. |
webauthn: clientDataJSON challenge does not match the issued challenge | 401 | The authenticator signed a different challenge than the one we issued. Almost always two overlapping ceremonies in the same tab. |
webauthn: clientDataJSON origin is not an allowed origin | 401 | Address mismatch — see the origin section. The message names the allowed list. |
webauthn: relying-party ID hash does not match | 401 | The credential belongs to a different domain. |
webauthn: unknown credential | 401 | We hold no credential with that identifier. Usually a credential deleted on our side but still offered by the device. |
signature verification failed | 401 | The signature did not verify against the stored public key. Not something a legitimate authenticator produces. |
webauthn: authenticator did not assert user presence | 401 | Nobody touched the key. Tap the security key, or complete the biometric prompt. |
webauthn: user verification required but not performed | 401 | A policy demanded verification — a PIN or biometric — and the device did not perform it. Set a PIN on the security key. |
webauthn: sign counter went backwards — possible cloned authenticator | 401 | Counter regression. See above. |
webauthn: ceremony failed: ceremony was performed cross-origin | 400 | The ceremony ran inside a frame on another site. That is the shape of a clickjacked enrolment and it is refused. Open the console in its own tab. |
webauthn: ceremony failed: challenge <id> is a <type> ceremony | 400 | A registration response was submitted to a sign-in endpoint, or the reverse. A client wiring error. |
webauthn: ceremony failed: clientDataJSON type is "webauthn.get", expected "webauthn.create" | 400 | The same mix-up, seen from the browser's side of the exchange. |
webauthn: ceremony failed: credential belongs to a different user than the challenge | 400 | The credential presented is not the challenged person's. Refused rather than resolved in the caller's favour. |
webauthn: ceremony failed: user handle does not match the registered credential | 400 | Username-less sign-in where the device's stored handle disagrees with ours. Re-register the passkey. |
webauthn: ceremony failed: authenticator data carries no attested credential data | 400 | A registration response with no credential in it. A browser or client bug; try another browser. |
webauthn: ceremony failed: credential_id is required | 400 | The client omitted the identifier. |
webauthn: ceremony failed: clientDataJSON is not valid base64url | 400 | Encoding damage in transit. The sibling messages are attestationObject is required and must be base64url, client_data_json is not valid base64url and signature is not valid base64url. |
webauthn: ceremony failed: packed attestation declares alg 7 but the credential key is alg -7 | 400 | The authenticator's attestation disagrees with the key it produced. Rare, and hardware-specific. |
no passkey is registered for this user | 409 | Not a failure — the person has no passkey yet. Send them to registration rather than to sign-in. |
auth required | 401 | A registration or step-up call with no session. Registration always needs an existing session. |
credential not found | 404 | Renaming or deleting a credential that is not there. |
Duplicate and unnamed credentials
Registration excludes the person's existing credentials, so the same authenticator cannot be silently enrolled twice — otherwise somebody ends up with three indistinguishable 'Security Key' entries and no way to tell which one they lost. If a re-registration is refused by the browser rather than by us, that exclusion is why. Rename credentials as you add them; a list of identical names is unusable at exactly the moment it matters.
- Passkeys and security keys — what they are and how to roll them out.
- MFA enrolment problems — the authenticator-app equivalent, including what to do when a device is lost.
- A user cannot sign in — if the block happens before the passkey prompt appears.