antiwork/gumroad · error · VerificationError

unknown_credential

unknown_credential

Error message

unknown_credential

What it means

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.

Source

Thrown at app/controllers/logins/passkeys_controller.rb:47

    sign_in(user)
    reset_two_factor_auth_login_session
    merge_guest_cart_with_user_cart
    refresh_passkey_setup_prompt(user)

    Rails.logger.info("passkey.authentication.succeeded user_id=#{user.id} webauthn_credential_id=#{stored_credential.id}")

    render json: { success: true, redirect_location: login_path_for(user) }
  rescue VerificationError => e
    log_authentication_failure(e.reason)
    render json: { success: false, error_message: AUTHENTICATION_ERROR_MESSAGE }, status: :unprocessable_entity
  end

  private
    def verified_credential(challenge)
      map_webauthn_verification_errors do
        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

View on GitHub (pinned to afeacbd394)

Solutions

  1. If the passkey was deleted, re-register it: log in with password/2FA, then add a new passkey under Settings → Passkeys.
  2. Verify WebAuthn configuration (rp_id, origin) matches across environments — passkeys are bound to the relying party id.
  3. Clear the stale credential from the browser's saved passkeys (or the OS authenticator) so it stops being offered.
  4. Frontend: treat this 422 as "unknown credential" — offer account recovery or an alternative sign-in method instead of retry.

Example fix

# before — passkey removed server-side but still offered by the browser
user.webauthn_credentials.destroy(credential) # later: navigator.credentials.get offers it => unknown_credential

# after — when deleting server-side, also prevent resurface
def destroy
  @webauthn_credential.destroy!
  render json: { success: true }
end
# and in the login UI, fall back to password sign-in when the passkey POST 422s
Defensive patterns

Strategy: try-catch

Type guard

const isRegisteredCredentialId = (id) =>
  registeredCredentialIds.includes(id); // maintain client-side list from registration/last-known state

Try / catch

begin
  stored = WebauthnCredential.find_by_webauthn_id(webauthn_credential.id)
  raise VerificationError, "unknown_credential" if stored.nil?
rescue VerificationError => e
  # generic 422; client should fall back to another sign-in method
end

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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