Validating clientDataJSON Fields

clientDataJSON is the browser’s statement about the ceremony it just ran, and it is the only place the challenge and the origin appear in a form your server can check. This page covers the fields, the exact comparisons each one requires, and the parsing mistake that makes a valid signature fail. It sits under The Challenge-Response Authentication Flow, which explains where the challenge comes from in the first place.


Field Reference

Field Type Check Failure means
challenge base64url string equals the challenge you issued, still unconsumed replay, or a stale ceremony
origin string exact match against your allow-list wrong surface, or a genuine cross-origin attempt
type string webauthn.create or webauthn.get, matching the ceremony a registration response replayed as an assertion
crossOrigin boolean present when the ceremony ran in a frame decide whether your policy permits it
tokenBinding object legacy; ignore nothing — it is effectively unused

Unknown fields may appear. The specification permits clients to add them, so a parser that rejects unrecognised keys will break on a browser update.

The four fields, and what each one binds clientDataJSON binds a ceremony to a challenge, an origin, a ceremony type and — sometimes — a cross-origin flag. Four fields; three of them are mandatory checks challenge binds this response to the specific ceremony you started origin binds it to the page that ran it — exact string, port included type distinguishes a registration from an assertion; prevents cross-ceremony replay crossOrigin tells you the ceremony ran inside a cross-origin frame

Root Cause Analysis

1. Re-serialising before hashing. The signature covers a hash of the exact bytes the client produced. Parsing the JSON into an object and stringifying it again produces a different byte sequence — different key order, different whitespace, different escaping — and the signature fails. This is the most common cause of an “invalid signature” that is not a signature problem at all.

2. Loose origin comparison. A check that tests whether the origin contains or starts with your domain will accept https://example.com.attacker.net. Origin comparison must be exact string equality against a configured list.

3. Forgetting the port. The origin string includes the port whenever it is non-default. A development or staging surface running on a non-standard port will fail against an allow-list built from hostnames, and the usual fix — stripping the port — silently weakens production.

4. Not checking type. Without it, a registration response can be presented to an authentication endpoint. The signature verifies, because the same key signed it; only the type field distinguishes the two ceremonies.

5. Trusting crossOrigin to be absent. If your policy forbids framed ceremonies, check the flag explicitly rather than assuming the field will not appear.

Compare, do not parse and rebuild The JSON is parsed for comparison only; the bytes that were signed must be hashed exactly as received. Two uses of the same value, and they must not be mixed raw bytes as received, untouched parse a copy for field comparison compare challenge, origin, type hash the original for signature verification Hashing a re-serialised copy is the single most common cause of a signature that will not verify against a perfectly good key.

Step-by-Step Resolution

Step 1 — keep the raw bytes

Whatever else happens, retain the buffer exactly as received. Everything downstream either compares against a parsed copy or hashes the original; nothing rewrites it.

Step 2 — decode with the canonical base64url helper

The value arrives base64url-encoded without padding. Use the same helper as the rest of your verification path so a padding quirk cannot produce different bytes here than elsewhere.

Step 3 — compare the challenge against the record you consumed

Look up and atomically consume the challenge first, then compare. Comparing before consuming leaves a window in which two concurrent requests both succeed.

Step 4 — compare the origin exactly

Equality against a configured allow-list. No prefix matching, no hostname extraction, no deriving the expected value from the incoming request.

Step 5 — assert the ceremony type

Registration endpoints accept only webauthn.create; authentication endpoints accept only webauthn.get. A mismatch is a hard rejection.

Step 6 — hash the untouched bytes

Compute SHA-256 over the original buffer and concatenate it after authenticatorData for signature verification.

Exact match, not fuzzy match Origin comparison is a byte-for-byte string equality against an allow-list, not a hostname or prefix test. The comparison that origin binding actually depends on correct equality against an allow-list scheme, host and port all included the list is configuration, not input a miss is a hard rejection weakening approximations startsWith or endsWith checks comparing only the hostname deriving the expected value from the request normalising case or trailing slashes Every approximation on the right has appeared in a real advisory; the port omission is the most frequent.

Verification and Testing

The highest-value test compares a digest of the raw buffer at ingress with a digest computed immediately before verification. If they differ, something between the two points is rewriting the value — a middleware, a serialiser, a proxy — and no amount of examining the cryptography will reveal it.

Test the origin comparison with three fixtures: the exact allowed origin, the same origin with a different port, and a look-alike domain that contains the allowed one as a substring. The third is what catches a prefix or contains check that a code review will read straight past.

Test the type check by replaying a captured registration response against your authentication endpoint. It should be rejected on the type field, before any signature verification runs.


Pitfalls

1. Deriving the expected origin from the request. The value then agrees with whatever the client sent, which is the opposite of a check.

2. Case-normalising the origin. Origins are compared as-is; normalising invites a mismatch with the allow-list you built from configuration.

3. Rejecting unknown fields. Clients may add them. Ignore what you do not recognise.


Frequently Asked Questions

Why does the challenge appear both in my store and in clientDataJSON?

Because the comparison is the point. Your store holds what you issued; the client returns what the authenticator signed over. Equality between the two is what proves the response belongs to the ceremony you started rather than to one captured earlier.

Should the origin allow-list include every subdomain?

It should include every origin that will legitimately run a ceremony, enumerated explicitly. A wildcard defeats the purpose, and the set is usually small enough to list — one or two production surfaces plus whatever staging needs.

What does crossOrigin being true actually mean?

That the ceremony ran inside a frame whose origin differs from the top-level document’s. It is permitted when the embedding page grants the appropriate permissions policy, and whether you accept it is your decision. Many relying parties refuse it outright, which is a defensible default.

Is the JSON guaranteed to be valid UTF-8?

Yes, and it is guaranteed to be JSON — but not to have a particular key order or whitespace, which is exactly why you must not re-serialise it. Parse a copy for comparison and hash the original.

Does the token binding field matter?

No. It is a legacy field from a mechanism that was never widely deployed. Ignore it; do not fail on its presence or absence.

Can I log clientDataJSON?

Log the parsed origin and type freely — they are configuration-shaped values. Do not log the challenge, which is ceremony material, and be aware that logging the whole structure alongside the credential id gives you a record of who signed in from where, with the retention obligations that implies.

What if two of my surfaces need different origin lists?

Store the acceptable origins on the challenge record when it is issued, rather than looking them up at verification time. The ceremony is then checked against the expectations it was created with, and a configuration change deployed mid-ceremony cannot cause a mid-flight failure.


Where should these comparisons live in the code?

In one function that takes the raw bytes and the expectations, and returns either a decoded structure or a typed failure naming which comparison failed. Scattering the checks across a request handler is how one of them ends up conditional, and a conditional origin check is indistinguishable from no origin check in every code review that has ever missed one. Keeping them together also gives you a single place to emit the internal reason code, which is what makes the resulting telemetry readable.

Does any of this change for a cross-origin ceremony?

The comparisons themselves do not: the challenge, the type and the origin are checked exactly as described. What changes is the interpretation of the origin, because in a framed ceremony it is the frame’s origin rather than the top-level page’s. A relying party that permits framed ceremonies therefore needs the frame’s origin in its allow-list, and should check the cross-origin flag explicitly so that permission is a deliberate configuration rather than an accident of which origins happen to be listed.

Related