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

  1. 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".
  2. Verify clientDataJSON contains type, challenge, and origin as strings after base64url-decode; mismatches mean the client used the wrong options or origin.
  3. Redeploy the current JS bundle if serialization drifted (asset/server code skew), then retry adding the passkey.
  4. 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

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

Related errors


AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21). Data as JSON: /api/errors/22026a34ee14101f. Report an issue: GitHub.