{"record":{"id":"b2390cd9716f5ea6","repo":"antiwork/gumroad","slug":"unknown-credential","errorCode":"unknown_credential","errorMessage":"unknown_credential","messagePattern":"unknown_credential","errorType":"error_code","errorClass":"VerificationError","httpStatus":422,"severity":"error","filePath":"app/controllers/logins/passkeys_controller.rb","lineNumber":47,"sourceCode":"    sign_in(user)\n    reset_two_factor_auth_login_session\n    merge_guest_cart_with_user_cart\n    refresh_passkey_setup_prompt(user)\n\n    Rails.logger.info(\"passkey.authentication.succeeded user_id=#{user.id} webauthn_credential_id=#{stored_credential.id}\")\n\n    render json: { success: true, redirect_location: login_path_for(user) }\n  rescue VerificationError => e\n    log_authentication_failure(e.reason)\n    render json: { success: false, error_message: AUTHENTICATION_ERROR_MESSAGE }, status: :unprocessable_entity\n  end\n\n  private\n    def verified_credential(challenge)\n      map_webauthn_verification_errors do\n        webauthn_credential = WebAuthn::Credential.from_get(assertion_params)\n        stored_credential = WebauthnCredential.find_by_webauthn_id(webauthn_credential.id)\n        raise VerificationError, \"unknown_credential\" if stored_credential.nil?\n\n        webauthn_credential.verify(\n          challenge,\n          public_key: stored_credential.public_key,\n          sign_count: stored_credential.sign_count,\n          user_verification: true\n        )\n\n        stored_credential.assign_attributes(sign_count: webauthn_credential.sign_count, last_used_at: Time.current)\n        stored_credential\n      end\n    end\n\n    def assertion_params\n      permitted_params = permitted_credential_params(response: [:authenticatorData, :clientDataJSON, :signature, :userHandle])\n      raise VerificationError, \"malformed_credential\" unless valid_assertion_params?(permitted_params)\n\n      permitted_params","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/controllers/logins/passkeys_controller.rb#L29-L65","documentation":"Raised in `Logins::PasskeysController#verified_credential` (logins/passkeys_controller.rb:47): `WebAuthn::Credential.from_get` parsed the assertion, but `WebauthnCredential.find_by_webauthn_id(webauthn_credential.id)` found no stored credential with that WebAuthn id. The authenticator presented a key this server never registered (or no longer stores), so there is nothing to verify against — the controller returns the generic 422.","triggerScenarios":"User removed the passkey in Settings → Passkeys from another device, then tries it here; the passkey was created for a different relying party (rp_id) so its id exists but not in this store; browser autofills a passkey belonging to another account/environment (staging vs production); the credential row was purged with account deletion.","commonSituations":"Deleted passkey still cached in the browser's credential manager; rp_id/rp_name config drift between environments (a passkey minted for staging rp used on production); users with multiple Gumroad accounts where the wrong one is offered.","solutions":["If the passkey was deleted, re-register it: log in with password/2FA, then add a new passkey under Settings → Passkeys.","Verify WebAuthn configuration (rp_id, origin) matches across environments — passkeys are bound to the relying party id.","Clear the stale credential from the browser's saved passkeys (or the OS authenticator) so it stops being offered.","Frontend: treat this 422 as \"unknown credential\" — offer account recovery or an alternative sign-in method instead of retry."],"exampleFix":"# before — passkey removed server-side but still offered by the browser\nuser.webauthn_credentials.destroy(credential) # later: navigator.credentials.get offers it => unknown_credential\n\n# after — when deleting server-side, also prevent resurface\ndef destroy\n  @webauthn_credential.destroy!\n  render json: { success: true }\nend\n# and in the login UI, fall back to password sign-in when the passkey POST 422s","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"const isRegisteredCredentialId = (id) =>\n  registeredCredentialIds.includes(id); // maintain client-side list from registration/last-known state","tryCatchPattern":"begin\n  stored = WebauthnCredential.find_by_webauthn_id(webauthn_credential.id)\n  raise VerificationError, \"unknown_credential\" if stored.nil?\nrescue VerificationError => e\n  # generic 422; client should fall back to another sign-in method\nend","preventionTips":["After deleting a passkey server-side, also remove it from the browser/OS credential manager when possible.","Keep WebAuthn rp_id and origins identical across environments; passkeys are RP-bound.","On 422, offer password/2FA sign-in and passkey re-registration rather than retry.","During auth-failure spikes, grep logs for reason=unknown_credential to distinguish config drift from user error."],"tags":["webauthn","passkeys","unknown-credential","authentication","relying-party"],"backgroundTag":"webauthn-unknown-credential","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}