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 length

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Set rp_origin in the WebAuthn config to the exact origin the frontend is served from, including scheme and port
  2. Redeploy/restart the runtime so the new config is loaded
  3. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/3424d36bd4b96c9c. Report an issue: GitHub.