{"record":{"id":"22026a34ee14101f","repo":"antiwork/gumroad","slug":"malformed-credential-22026a","errorCode":"malformed_credential","errorMessage":"malformed_credential","messagePattern":"malformed_credential","errorType":"error_code","errorClass":"VerificationError","httpStatus":422,"severity":"error","filePath":"app/controllers/settings/passkeys_controller.rb","lineNumber":106,"sourceCode":"  private\n    def set_user\n      @user = current_seller\n    end\n\n    def authorize\n      super([:settings, :passkeys, @user])\n    end\n\n    def set_webauthn_credential\n      @webauthn_credential = @user.webauthn_credentials.find_by_external_id(params[:id]) || e404\n    end\n\n    def credential_params\n      permitted_params = permitted_credential_params(\n        response: [:attestationObject, :clientDataJSON, { transports: [] }],\n        clientExtensionResults: {}\n      )\n      raise VerificationError, \"malformed_credential\" unless valid_credential_params?(permitted_params)\n\n      permitted_params\n    end\n\n    def verified_webauthn_credential(challenge)\n      map_webauthn_verification_errors do\n        WebAuthn::Credential.from_create(credential_params).tap do |credential|\n          credential.verify(challenge, user_verification: true)\n        end\n      end\n    end\n\n    def detected_provider_name(webauthn_credential)\n      WebauthnCredential.provider_name_for_aaguid(webauthn_credential.response&.authenticator_data&.aaguid)\n    end\n\n    def passkey_props(credential)\n      {","sourceCodeStart":88,"sourceCodeEnd":124,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/controllers/settings/passkeys_controller.rb#L88-L124","documentation":"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.\"","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before — attestationObject sent as plain base64 with +/\nbody: 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\" } } })\n\n// after — base64url everywhere, transports as array\nconst b64u = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf))).replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\nbody: 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: {} } })","handlingStrategy":"validation","validationCode":"// client, before POST /settings/passkeys\nconst b64url = (s) => typeof s === \"string\" && /^[A-Za-z0-9_-]+={0,2}$/.test(s);\nconst resp = c.response;\nif (!(c.type === \"public-key\" && b64url(c.id) && b64url(c.rawId) &&\n      b64url(resp?.attestationObject) && b64url(resp?.clientDataJSON) &&\n      Array.isArray(resp?.transports))) throw new Error(\"bad attestation shape\");","typeGuard":"const isBase64url = (v) => typeof v === \"string\" && /^[A-Za-z0-9_-]+={0,2}$/.test(v);\nconst isValidAttestation = (c) =>\n  c.type === \"public-key\" &&\n  isBase64url(c.id) && isBase64url(c.rawId) &&\n  c.response instanceof Object &&\n  isBase64url(c.response.attestationObject) &&\n  isBase64url(c.response.clientDataJSON) &&\n  c.clientExtensionResults instanceof Object;","tryCatchPattern":"begin\n  credential = verified_webauthn_credential(challenge)\nrescue VerificationError => e\n  log_registration_failure(e.reason) # malformed_credential\n  render json: { success: false, error_message: \"Could not add this passkey. Please try again.\" }, status: :unprocessable_entity\nend","preventionTips":["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."],"tags":["webauthn","passkeys","attestation","base64url","params-validation"],"backgroundTag":"webauthn-malformed-credential","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}