zeroclaw-labs/zeroclaw · error
Origin mismatch: expected '{}', got '{cd_origin}'
Error message
Origin mismatch: expected '{}', got '{cd_origin}' What it means
finish_registration verifies client_data_json origin equals the configured rp_origin for the relying party. The client's actual origin (scheme + host + port) differs from the configured value, so the response is rejected as coming from an unexpected origin.
Source
Thrown at crates/zeroclaw-runtime/src/security/webauthn.rs:290
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 the
// credential public key. The attestation object is CBOR-encoded but
// for our minimal implementation we accept a simplified JSON format
// from our enrollment UI, or parse the raw CBOR authData.
let (public_key_bytes, sign_count) =
extract_public_key_from_attestation(&attestation_bytes)?;
// 3. Validate credential ID lengthView on GitHub (pinned to 88bb9c8533)
Solutions
- Set rp_origin in the WebAuthn config to the exact origin the frontend is served from, including scheme and port
- Redeploy/restart the runtime so the new config is loaded
- If you must serve multiple frontends, align on one domain or run separate relying-party configurations
Example fix
# before rp_origin = "http://localhost:3000" # prod frontend is https://app.example.com # after rp_origin = "https://app.example.com"
Defensive patterns
Strategy: validation
Validate before calling
// fail fast before the ceremony if origins can never match
const rpOrigin = serverConfig.rp_origin; // e.g. 'https://app.example.com'
if (window.location.origin !== rpOrigin) {
throw new Error(`page origin ${window.location.origin} != configured rp_origin ${rpOrigin}`);
} Try / catch
map the origin-mismatch message to HTTP 400 and include both expected and received origins in the response so misconfiguration is obvious in one round trip
Prevention
- Derive rp_origin from the same environment variable the frontend build uses
- Use environment-specific configs for dev/staging/prod
- Remember origin includes scheme and explicit ports (localhost:3000 differs from localhost)
When it happens
Trigger: rp_origin configured as https://api.example.com while the frontend runs on https://app.example.com or http://localhost:3000; http vs https, trailing port (:443 explicit vs implicit), or a proxy/TLS terminator changing the scheme; frontend moved to a new domain without updating config.
Common situations: Local development against a production-configured server; deployments behind reverse proxies; migration to a new domain or port; forgetting the origin includes scheme and port.
Related errors
- Unable to extract public key from attestation object ({} byt
- Expected type 'webauthn.create', got '{cd_type}'
- Challenge mismatch in registration response
- Credential ID too long ({} bytes, max {MAX_CREDENTIAL_ID_LEN
- Gmail OAuth token is not configured
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/3424d36bd4b96c9c.
Report an issue: GitHub.