{"record":{"id":"5d5537590bba5027","repo":"we-promise/sure","slug":"credential-must-be-an-object","errorCode":null,"errorMessage":"credential must be an object","messagePattern":"credential must be an object","errorType":"http","errorClass":"ActionController::BadRequest","httpStatus":400,"severity":"error","filePath":"app/controllers/concerns/webauthn_relying_party.rb","lineNumber":25,"sourceCode":"    def webauthn_relying_party\n      webauthn_config = Rails.application.config.x.webauthn\n\n      WebAuthn::RelyingParty.new(\n        name: \"Sure\",\n        id: webauthn_config.rp_id,\n        allowed_origins: webauthn_config.allowed_origins,\n        # Accept consumer passkeys/security keys without attesting device vendor\n        # identity; this keeps MFA registration broad for self-hosted users.\n        verify_attestation_statement: false\n      )\n    end\n\n    def webauthn_credential_payload\n      payload = params.require(:credential)\n      payload = JSON.parse(payload) if payload.is_a?(String)\n\n      payload = payload.to_unsafe_h if payload.respond_to?(:to_unsafe_h)\n      raise ActionController::BadRequest, \"credential must be an object\" unless payload.is_a?(Hash)\n\n      payload\n    rescue JSON::ParserError, TypeError, ArgumentError\n      raise ActionController::BadRequest, \"invalid credential payload\"\n    end\nend\n","sourceCodeStart":7,"sourceCodeEnd":32,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/controllers/concerns/webauthn_relying_party.rb#L7-L32","documentation":"WebAuthn registration/authentication sends the browser's credential response as the credential param. The concern's webauthn_credential_payload helper accepts it as JSON (string) or as nested params, normalizes ActionController::Parameters via to_unsafe_h, and then requires the result to be a Hash — because WebAuthn::Credential.from_json needs a JSON object with fields like id/rawId/type/response. If the parsed payload is an Array, String, number, or nil, it raises ActionController::BadRequest (HTTP 400) with \"credential must be an object\".","triggerScenarios":"POSTing the credential param as a JSON string that parses to an array or scalar (e.g. credential='[\"abc\"]' or credential='\"abc\"'); sending form-encoded params that make credential a bare string like credential=hello instead of nested fields (credential[id]=…); a client double-encoding so the outer parse yields a non-object; tests/fixtures that stub the param with a plain token instead of the full PublicKeyCredential JSON.","commonSituations":"Custom passkey UIs that build the request body by hand instead of using the serialization the frontend library (e.g. @simplewebauthn/browser) produces; fetch with a wrong Content-Type so Rails stringifies the body; load balancer or middleware mutating the body; copying a curl example from another app that sends the id only.","solutions":["Send the complete PublicKeyCredential object exactly as navigator.credentials.get()/create() returns it — JSON.stringify the whole response into the credential param, or nest its fields as credential[id], credential[rawId], credential[type], credential[response][…]","Confirm the request Content-Type matches how you're sending it (application/json body vs form params) so Rails doesn't collapse it to a string","In tests, pass a fixture of the real credential JSON object, not a placeholder string","Reproduce the normalization: payload = JSON.parse(str); payload.is_a?(Hash) or raise"],"exampleFix":"# before (broken fetch)\nfetch(url, { method: \"POST\", body: \"credential=\" + assertion.id })\n# server: credential=\"Y3JlZA\" -> parsed to a String -> 400 \"credential must be an object\"\n\n# after\nconst res = await navigator.credentials.get({ publicKey: options });\nfetch(url, {\n  method: \"POST\",\n  headers: { \"Content-Type\": \"application/json\" },\n  body: JSON.stringify({ credential: JSON.stringify(res) }) // full object, parses to Hash\n});","handlingStrategy":"validation","validationCode":"# Client-side, before POSTing\nfunction isCredentialObject(v) {\n  if (typeof v === \"string\") { try { v = JSON.parse(v); } catch { return false; } }\n  return v !== null && typeof v === \"object\" && !Array.isArray(v);\n}\nif (!isCredentialObject(credential)) throw new Error(\"credential must be a JSON object\");","typeGuard":"function isCredentialObject(v) {\n  if (typeof v === \"string\") { try { v = JSON.parse(v); } catch { return false; } }\n  return v !== null && typeof v === \"object\" && !Array.isArray(v);\n}","tryCatchPattern":"rescue ActionController::BadRequest\n  # 400 already sent by Rails; log shape of params (never the credential secrets)\n  # and prompt the client to re-send the full PublicKeyCredential object\n  head :bad_request\nend","preventionTips":["Use the official WebAuthn JS library's serialization; never build the payload by hand","Always JSON.stringify the entire PublicKeyCredential from navigator.credentials","Keep the request Content-Type consistent with the body format you send","In tests, use recorded real credential JSON objects, never placeholder strings"],"tags":["rails","webauthn","passkeys","request-payload","bad-request"],"backgroundTag":"invalid-request-payload","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}