antiwork/gumroad · error · VerificationError
malformed_credential
malformed_credential
Error message
malformed_credential
What it means
Raised in `Logins::PasskeysController#assertion_params` (logins/passkeys_controller.rb:63): the credential arrived as an object, but its fields failed `valid_assertion_params?` — `type` must equal "public-key", `id`/`rawId` must be base64url-encoded strings, and `response` must be a hash whose `authenticatorData`, `clientDataJSON`, and `signature` are base64url (`userHandle` optional). Any deviation raises `VerificationError("malformed_credential")` → generic 422.
Source
Thrown at app/controllers/logins/passkeys_controller.rb:63
webauthn_credential = WebAuthn::Credential.from_get(assertion_params)
stored_credential = WebauthnCredential.find_by_webauthn_id(webauthn_credential.id)
raise VerificationError, "unknown_credential" if stored_credential.nil?
webauthn_credential.verify(
challenge,
public_key: stored_credential.public_key,
sign_count: stored_credential.sign_count,
user_verification: true
)
stored_credential.assign_attributes(sign_count: webauthn_credential.sign_count, last_used_at: Time.current)
stored_credential
end
end
def assertion_params
permitted_params = permitted_credential_params(response: [:authenticatorData, :clientDataJSON, :signature, :userHandle])
raise VerificationError, "malformed_credential" unless valid_assertion_params?(permitted_params)
permitted_params
end
def valid_assertion_params?(permitted_params)
response = permitted_params["response"]
valid_credential_base?(permitted_params) &&
base64url_encoded?(response["authenticatorData"]) &&
base64url_encoded?(response["clientDataJSON"]) &&
base64url_encoded?(response["signature"])
end
def log_authentication_failure(reason)
log_ceremony_failure("authentication", reason)
end
end
View on GitHub (pinned to afeacbd394)
Solutions
- Serialize exactly per the WebAuthn JSON convention: id/rawId and response fields as base64url (RFC 4648 §5, no +,/), type: "public-key".
- Use a maintained serializer (e.g. @github/webauthn-json style helpers) rather than hand-written encoding.
- Check every response field is a non-empty base64url string before POSTing; log which field fails in dev.
- Keep the JS bundle deployed atomically with the server params contract (the same lesson as the dispute-evidence singular/plural param note in this codebase).
Example fix
// before — rawId left as a byte array and wrong type
body: JSON.stringify({ credential: { id: cred.id, rawId: Array.from(new Uint8Array(cred.rawId)), type: "webauthn.get", response: { ... } } })
// after
const b64u = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
body: JSON.stringify({ credential: { id: cred.id, rawId: b64u(cred.rawId), type: "public-key", response: { authenticatorData: b64u(cred.response.authenticatorData), clientDataJSON: b64u(cred.response.clientDataJSON), signature: b64u(cred.response.signature), userHandle: cred.response.userHandle ? b64u(cred.response.userHandle) : undefined } } }) Defensive patterns
Strategy: validation
Validate before calling
// client, before POST
const b64url = (s) => typeof s === "string" && /^[A-Za-z0-9_-]+={0,2}$/.test(s);
const ok =
cred.type === "public-key" &&
b64url(cred.id) && b64url(cred.rawId) &&
b64url(cred.response?.authenticatorData) &&
b64url(cred.response?.clientDataJSON) &&
b64url(cred.response?.signature);
if (ok) await submit(cred); Type guard
const isBase64url = (v) => typeof v === "string" && /^[A-Za-z0-9_-]+={0,2}$/.test(v);
const isValidAssertion = (c) =>
c.type === "public-key" &&
isBase64url(c.id) && isBase64url(c.rawId) &&
c.response instanceof Object &&
isBase64url(c.response.authenticatorData) &&
isBase64url(c.response.clientDataJSON) &&
isBase64url(c.response.signature); Try / catch
begin
params = assertion_params
rescue VerificationError => e
log_authentication_failure(e.reason) # reason=malformed_credential in logs
render json: { success: false, error_message: AUTHENTICATION_ERROR_MESSAGE }, status: :unprocessable_entity
end Prevention
- Use one shared, tested base64url serializer for all WebAuthn fields across the app.
- Never mix standard base64 (+/) into base64url fields; strip padding consistently.
- Add a client-side shape assertion before POST to fail fast with a specific message.
- Keep the JS bundle and server param contract versioned and deployed together.
When it happens
Trigger: Client sends `type` missing or not "public-key"; ArrayBuffer fields serialized as hex/plain objects instead of base64url; rawId sent as an array of bytes; response fields left as empty strings or omitted; a hand-rolled client re-encoding fields with standard base64 (+/) instead of base64url (-_) or padding errors.
Common situations: Custom serialization helpers in older JS bundles; polyfills that stringify ArrayBuffers differently; proxies/transformers that URL-decode and corrupt base64url characters; test fixtures with hardcoded non-base64url values.
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/7bff2d071a7c9cf3.
Report an issue: GitHub.