antiwork/gumroad · error · VerificationError
malformed_credential
malformed_credential
Error message
malformed_credential
What it means
Raised in `Settings::PasskeysController#credential_params` (settings/passkeys_controller.rb:106) during passkey REGISTRATION: the attestation payload failed `valid_credential_params?` (settings/passkeys_controller.rb:140-168) — base fields invalid (type ≠ "public-key", id/rawId not base64url), `response.attestationObject` not decoding to a CBOR map with fmt/attStmt/authData, or `response.clientDataJSON` not decoding to JSON with type/challenge/origin. The controller rescues VerificationError → 422 "Could not add this passkey. Please try again."
Source
Thrown at app/controllers/settings/passkeys_controller.rb:106
private
def set_user
@user = current_seller
end
def authorize
super([:settings, :passkeys, @user])
end
def set_webauthn_credential
@webauthn_credential = @user.webauthn_credentials.find_by_external_id(params[:id]) || e404
end
def credential_params
permitted_params = permitted_credential_params(
response: [:attestationObject, :clientDataJSON, { transports: [] }],
clientExtensionResults: {}
)
raise VerificationError, "malformed_credential" unless valid_credential_params?(permitted_params)
permitted_params
end
def verified_webauthn_credential(challenge)
map_webauthn_verification_errors do
WebAuthn::Credential.from_create(credential_params).tap do |credential|
credential.verify(challenge, user_verification: true)
end
end
end
def detected_provider_name(webauthn_credential)
WebauthnCredential.provider_name_for_aaguid(webauthn_credential.response&.authenticator_data&.aaguid)
end
def passkey_props(credential)
{View on GitHub (pinned to afeacbd394)
Solutions
- Ensure the browser flow runs the standard registration: GET /settings/passkeys/registration_options → navigator.credentials.create → POST with base64url-serialized attestation (attestationObject, clientDataJSON) and type "public-key".
- Verify clientDataJSON contains type, challenge, and origin as strings after base64url-decode; mismatches mean the client used the wrong options or origin.
- Redeploy the current JS bundle if serialization drifted (asset/server code skew), then retry adding the passkey.
- If it persists on one device only, remove conflicting passkey-manager extensions and retry, or register from another browser.
Example fix
// before — attestationObject sent as plain base64 with +/
body: JSON.stringify({ credential: { id: c.id, rawId: btoa(c.rawId), type: "public-key", response: { attestationObject: btoa(c.response.attestationObject), clientDataJSON: btoa(c.response.clientDataJSON), transports: "internal" } } })
// after — base64url everywhere, transports as array
const b64u = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
body: JSON.stringify({ credential: { id: c.id, rawId: b64u(c.rawId), type: "public-key", response: { attestationObject: b64u(c.response.attestationObject), clientDataJSON: b64u(c.response.clientDataJSON), transports: ["internal"] }, clientExtensionResults: {} } }) Defensive patterns
Strategy: validation
Validate before calling
// client, before POST /settings/passkeys
const b64url = (s) => typeof s === "string" && /^[A-Za-z0-9_-]+={0,2}$/.test(s);
const resp = c.response;
if (!(c.type === "public-key" && b64url(c.id) && b64url(c.rawId) &&
b64url(resp?.attestationObject) && b64url(resp?.clientDataJSON) &&
Array.isArray(resp?.transports))) throw new Error("bad attestation shape"); Type guard
const isBase64url = (v) => typeof v === "string" && /^[A-Za-z0-9_-]+={0,2}$/.test(v);
const isValidAttestation = (c) =>
c.type === "public-key" &&
isBase64url(c.id) && isBase64url(c.rawId) &&
c.response instanceof Object &&
isBase64url(c.response.attestationObject) &&
isBase64url(c.response.clientDataJSON) &&
c.clientExtensionResults instanceof Object; Try / catch
begin
credential = verified_webauthn_credential(challenge)
rescue VerificationError => e
log_registration_failure(e.reason) # malformed_credential
render json: { success: false, error_message: "Could not add this passkey. Please try again." }, status: :unprocessable_entity
end Prevention
- Always start registration from /settings/passkeys/registration_options and echo its challenge via the browser's create() call — don't build clientDataJSON by hand.
- Serve the JS bundle that matches the server's param contract; watch asset/server deploy skew.
- Base64url-encode attestationObject and clientDataJSON; send transports as an array and clientExtensionResults as an object.
- In tests, generate fixtures from a real create() response or a maintained serializer.
When it happens
Trigger: POST /settings/passkeys with an attestation whose attestationObject is hex/base64 (not base64url CBOR), clientDataJSON missing the origin or challenge field, rawId double-stringified, or transports sent as a string instead of an array; also fires when a stale JS bundle serializes registration responses in an old shape.
Common situations: Same class of client bugs as assertion malformed_credential, plus CBOR-specific ones: older authenticator polyfills, browser extensions mutating the credential, staging/prod origin mismatch baked into clientDataJSON failing its shape check, hand-built test fixtures with real base64 instead of base64url.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- malformed_credential
- Could not add this passkey. Please try again.
- Sorry, something went wrong. Please try again.
- We couldn't sign you in with that passkey. Please try again
- missing_challenge
AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21).
Data as JSON: /api/errors/22026a34ee14101f.
Report an issue: GitHub.