zeroclaw-labs/zeroclaw · error
Challenge mismatch in registration response
Error message
Challenge mismatch in registration response
What it means
finish_registration compares the challenge inside client_data_json against the challenge stored in the pending AuthenticationState/registration state created by register start. A mismatch means the response was not produced for this ceremony instance — expired state, a second start overwriting the first, or encoding differences.
Source
Thrown at crates/zeroclaw-runtime/src/security/webauthn.rs:283
response: &RegisterCredentialResponse,
) -> Result<WebAuthnCredential> {
// 1. Validate client data JSON
let client_data_bytes = URL_SAFE_NO_PAD
.decode(&response.client_data_json)
.context("Invalid base64url in client_data_json")?;
let client_data: serde_json::Value =
serde_json::from_slice(&client_data_bytes).context("Invalid client data JSON")?;
// Verify type
let cd_type = client_data["type"].as_str().unwrap_or_default();
anyhow::ensure!(
cd_type == "webauthn.create",
"Expected type 'webauthn.create', got '{cd_type}'"
);
// Verify challenge matches
let cd_challenge = client_data["challenge"].as_str().unwrap_or_default();
anyhow::ensure!(
cd_challenge == reg_state.challenge,
"Challenge mismatch in registration response"
);
// Verify origin
let cd_origin = client_data["origin"].as_str().unwrap_or_default();
anyhow::ensure!(
cd_origin == self.config.rp_origin,
"Origin mismatch: expected '{}', got '{cd_origin}'",
self.config.rp_origin
);
// 2. Parse attestation object to extract public key and auth data
let attestation_bytes = URL_SAFE_NO_PAD
.decode(&response.attestation_object)
.context("Invalid base64url in attestation_object")?;
// For "none" attestation, we extract the authData which contains theView on GitHub (pinned to 88bb9c8533)
Solutions
- Restart the ceremony: call register start, then immediately finish with the fresh challenge
- Ensure exactly one start per registration attempt and that finish uses the options from that same start
- Compare challenges using the identical base64url (no padding) encoding on both client and server
Example fix
// before: stale options reused after a re-render
const regOptions = cachedFromLastPageLoad;
// after: fetch fresh options, then create the credential
const regOptions = await fetch('/register/start').then(r => r.json()); Defensive patterns
Strategy: validation
Validate before calling
// client: create the credential with the exact challenge bytes from the latest start call
const opts = await fetch('/register/start', { cache: 'no-store' }).then(r => r.json());
const cred = await navigator.credentials.create({ publicKey: opts });
// server: confirm state freshness
if reg_state.created_at + TTL < now { /* re-run start instead of finish */ } Try / catch
catch the mismatch; respond 400 with a 'restart-registration' code so the client calls start again, and invalidate the stored reg_state
Prevention
- Fetch registration options with cache: 'no-store' right before creating the credential
- Store one pending ceremony per user/session and expire it (TTL) so stale challenges are rejected early
- Use a single base64url encoding (unpadded) for challenges across client and server
When it happens
Trigger: Calling register start twice and finishing with options from the first call; the browser session restarting so server-side state was regenerated; encoding mismatches (raw vs base64url, padded vs unpadded challenge strings); replaying a captured finish payload.
Common situations: SPA flows that refetch registration options on re-render; multiple tabs each calling start; server storing one pending challenge per user that gets clobbered; clients re-encoding the challenge before hashing.
Related errors
- Challenge mismatch in authentication response
- Unable to extract public key from attestation object ({} byt
- Expected type 'webauthn.create', got '{cd_type}'
- Origin mismatch: expected '{}', got '{cd_origin}'
- Credential ID too long ({} bytes, max {MAX_CREDENTIAL_ID_LEN
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/91dfdc34b045b8da.
Report an issue: GitHub.