Skip to main content

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.

Written for whoever runs IT10 min readUpdated

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.

The passkey endpoints
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)
HTTP 401 on register/finish or login/finish

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.

Origin mismatches, what each looks like, and who can fix it. Only the first is yours.
SituationSymptomWho fixes it
A person reached the console through an unexpected address — a reverse proxy, a vanity domain, a link with a stale hostnameThe origin error names their address on the leftYou. Send them to the canonical address and the ceremony works.
The interface is served from a different host than the API's configured base URLEvery ceremony for every user fails on originUs. This is the wiring gap in the callout above. Report both hostnames.
The deployment's base URL is unparseable and fell back to localhostCeremonies 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 localhostUs. A deployment fault.
An empty allowed-origin list was configuredThe service refuses to start rather than accepting anything: webauthn: at least one allowed origin is requiredUs. 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
HTTP 401

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
HTTP 401

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

All passkey ceremony refusals with their HTTP status. Cryptographic failures return 401 with a specific reason; malformed input returns 400.
MessageStatusMeaning
challenge not found, already used, or expired401Challenges 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 challenge401The 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 origin401Address mismatch — see the origin section. The message names the allowed list.
webauthn: relying-party ID hash does not match401The credential belongs to a different domain.
webauthn: unknown credential401We hold no credential with that identifier. Usually a credential deleted on our side but still offered by the device.
signature verification failed401The signature did not verify against the stored public key. Not something a legitimate authenticator produces.
webauthn: authenticator did not assert user presence401Nobody touched the key. Tap the security key, or complete the biometric prompt.
webauthn: user verification required but not performed401A 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 authenticator401Counter regression. See above.
webauthn: ceremony failed: ceremony was performed cross-origin400The 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> ceremony400A 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"400The same mix-up, seen from the browser's side of the exchange.
webauthn: ceremony failed: credential belongs to a different user than the challenge400The 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 credential400Username-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 data400A registration response with no credential in it. A browser or client bug; try another browser.
webauthn: ceremony failed: credential_id is required400The client omitted the identifier.
webauthn: ceremony failed: clientDataJSON is not valid base64url400Encoding 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 -7400The authenticator's attestation disagrees with the key it produced. Rare, and hardware-specific.
no passkey is registered for this user409Not a failure — the person has no passkey yet. Send them to registration rather than to sign-in.
auth required401A registration or step-up call with no session. Registration always needs an existing session.
credential not found404Renaming 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.

Was this article wrong?

If a procedure here does not match what you see, or a limit we described has changed, tell us and we will fix the page. Email us about this article, or see how to get help if you need an answer rather than a correction.

Everything in troubleshooting