zeroclaw-labs/zeroclaw · error
Expected type 'webauthn.get', got '{cd_type}'
Error message
Expected type 'webauthn.get', got '{cd_type}' What it means
finish_authentication requires the client_data_json of an authentication response to carry type == "webauthn.get", the WebAuthn authentication ceremony type. A different value — commonly "webauthn.create" — means the payload belongs to the registration ceremony or was malformed, and authentication is rejected.
Source
Thrown at crates/zeroclaw-runtime/src/security/webauthn.rs:425
::zeroclaw_log::record!(
WARN,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
.with_outcome(::zeroclaw_log::EventOutcome::Failure)
.with_attrs(::serde_json::json!({"credential_id": response.id})),
"webauthn verify refused: credential id not in store"
);
anyhow::Error::msg(format!("Credential not found: {}", response.id))
})?;
// 3. 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")?;
let cd_type = client_data["type"].as_str().unwrap_or_default();
anyhow::ensure!(
cd_type == "webauthn.get",
"Expected type 'webauthn.get', got '{cd_type}'"
);
let cd_challenge = client_data["challenge"].as_str().unwrap_or_default();
anyhow::ensure!(
cd_challenge == auth_state.challenge,
"Challenge mismatch in authentication response"
);
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
);
// 4. Verify signatureView on GitHub (pinned to 88bb9c8533)
Solutions
- Send only navigator.credentials.get() results to the auth-finish endpoint
- Keep create/get response serializers separate; validate ceremony type client-side before posting
- In tests, generate client_data_json with type 'webauthn.get' and the challenge from auth start
Example fix
// before: registration response posted to login finish
const resp = await navigator.credentials.create({ publicKey: regOptions });
await fetch('/auth/finish', { method: 'POST', body: serialize(resp) });
// after
const resp = await navigator.credentials.get({ publicKey: authOptions });
await fetch('/auth/finish', { method: 'POST', body: serialize(resp) }); Defensive patterns
Strategy: try-catch
Try / catch
in the auth-finish HTTP handler, catch ceremony-validation errors (type/challenge/origin) and map to 400 with the message; do not retry the payload; client should call auth start again
Prevention
- Type the client responses (RegisterResponse vs AuthenticateResponse) so they cannot be cross-posted
- Drive real browser flows in e2e tests instead of hand-built JSON
- Log the offending client_data type on failure
When it happens
Trigger: Posting a navigator.credentials.create (registration) response to the auth-finish endpoint; hand-rolled test clients writing the wrong type string; client libraries or proxies that rewrite client_data_json.
Common situations: Frontend handlers wiring the create response into the login flow; e2e fixtures reusing registration client data for login; copy-paste errors in custom FIDO clients.
Related errors
- Expected type 'webauthn.create', got '{cd_type}'
- No registered credentials for user '{user_id}'
- Credential ID not in allowed list
- Challenge mismatch in authentication response
- createSession failed ({status}): {body}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/fc1e0df451fa6500.
Report an issue: GitHub.