Files
2026-05-27 23:47:53 +01:00

3.3 KiB

Client data — parse + validate

Purpose

Decodes the clientDataJSON byte string returned by navigator.credentials.create / get into a typed object and validates its type, origin, and challenge fields against the relying-party's expectations. The clientDataJSON is the authenticator's anti-replay anchor — getting this validation right is the difference between a real WebAuthn deployment and a token-passing protocol that any phishing site can replay.

Features

  • UTF-8 decode with fatal: true so any non-UTF-8 byte rejects with WEBAUTHN.MALFORMED_RESPONSE.
  • JSON parse with cause-chain preservation.
  • Zod ClientDataJSONSchema enforces type, challenge, origin fields; extra fields pass through for forward compatibility with future W3C revisions (topOrigin, crossOrigin, payment-extension fields).
  • Discriminated expectedType argument — "webauthn.create" for registration, "webauthn.get" for authentication — so callers cannot accidentally accept the wrong ceremony's response.
  • Constant-time origin and challenge compares via constantTimeEqual from @suaveplan/crypto-utils.
  • Base64url decode of challenge before compare so the byte-level comparison happens after canonicalisation.

Usage: Basic Example

import { parseClientDataJSON } from "@suaveplan/webauthn/parser";

const data = parseClientDataJSON(response.clientDataJSON);
console.log(data.type, data.origin);

Usage: Advanced Example

import {
    CLIENT_DATA_TYPE_CREATE,
    validateClientDataJSON,
} from "@suaveplan/webauthn/parser";

validateClientDataJSON({
    clientDataJSON: response.clientDataJSON,
    expectedType: CLIENT_DATA_TYPE_CREATE,
    expectedOrigin: "https://example.com",
    expectedChallenge: storedChallengeBytes,
});

API Reference

  • CLIENT_DATA_TYPE_CREATE = "webauthn.create" — registration discriminator.
  • CLIENT_DATA_TYPE_GET = "webauthn.get" — assertion discriminator.
  • parseClientDataJSON(bytes: Uint8Array): ClientDataJSON — decode + schema validate; throws WEBAUTHN.MALFORMED_RESPONSE.
  • validateClientDataJSON(input: { clientDataJSON, expectedType, expectedOrigin, expectedChallenge }): ClientDataJSON — full validation; throws WEBAUTHN.MALFORMED_RESPONSE (type), WEBAUTHN.ORIGIN_MISMATCH, or WEBAUTHN.CHALLENGE_MISMATCH on the appropriate failure.

Implementation Notes

  • The challenge is compared byte-for-byte after base64url decode rather than string-for-string so authenticators that emit base64url with or without padding (some Yubikey firmware does the latter on long challenges) all canonicalise to the same byte string.
  • origin is compared as UTF-8 bytes via constantTimeEqual rather than via === so a mismatch in the high-bit-set portion of an IDN origin (e.g. punycode-encoded host) takes time independent of the byte index of the difference.
  • The passthrough() on ClientDataJSONSchema is deliberate — the W3C spec reserves the right to add fields (tokenBinding, topOrigin, payment extension fields) and we do not want the parser to reject conforming new browsers.
  • parseClientDataJSON is the lower-level building block; consumers that already validated type / origin / challenge elsewhere can call it directly. validateClientDataJSON is the one-shot entry point used by register and authenticate.