instructure/canvas-lms · error · Lti::Errors::InvalidToolProxyError

Developer key mismatch

Error message

Developer key mismatch

What it means

Lti::ToolProxyService#create_tool_proxy raises Errors::InvalidToolProxyError with message "Developer key mismatch" when developer_key_mismatch?(tp, developer_key) is true — the developer key associated with the incoming tool proxy payload does not match the developer key supplied to the registration request. This prevents registering a tool proxy under the wrong key during LTI 2 registration.

Solutions

  1. Ensure the developer_key passed to process_tool_proxy_json matches the key bound to the tool proxy payload / registration request.
  2. If the key was rotated, redo the tool registration from scratch so the proxy is created under the current key.
  3. Check the stored tool_proxy record: if a proxy with the same guid already exists under another key, remove it or use the original key.
  4. Verify the registration URL flow isn't replayed with a stale payload referencing the old key.

Example fix

// before
service.process_tool_proxy_json(tp_json, developer_key: old_key)
// after
current_key = DeveloperKey.find(tp_json.dig('tool_proxy', 'security_contract')&.dig('developer_key')) || expected_key
service.process_tool_proxy_json(tp_json, developer_key: current_key)
Defensive patterns

Strategy: try-catch

Validate before calling

raise 'key mismatch' if developer_key && tp.developer_key && developer_key.global_id != tp.developer_key.global_id

Try / catch

begin
  service.process_tool_proxy_json(json, developer_key:)
rescue Lti::Errors::InvalidToolProxyError => e
  render json: { error: e.message }, status: :bad_request
end

Prevention

When it happens

Trigger: Processing tool proxy JSON (process_tool_proxy_json) where the ToolProxy's declared developer key GUID differs from the developer_key passed by the caller — e.g. the tool was registered with one key but the confirmation request carries another, or the key was rotated/changed between registration steps.

Common situations: Re-registering a tool after its developer key was replaced; using the wrong developer_key in the registration URL/callback; installing the same tool proxy guid against a different key on another account.

Related errors


AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15). Data as JSON: /api/errors/23460987b80520c6. Report an issue: GitHub.

Appendix: source

Thrown at app/models/lti/tool_proxy_service.rb:94

      installing_vendor = tool_proxy&.tool_profile&.product_instance&.product_info&.product_family&.vendor&.code
      return true if installing_vendor.blank?

      vendor_dev_keys = DeveloperKey.by_cached_vendor_code(installing_vendor)
      return false if developer_key.blank? && vendor_dev_keys.blank?

      !vendor_dev_keys.include?(developer_key)
    end

    def deprecated_split_secret?(tp)
      tp.enabled_capability.present? &&
        tp.enabled_capability.include?("OAuth.splitSecret") &&
        tp.security_contract.tp_half_shared_secret.present?
    end

    def create_tool_proxy(tp:, context:, product_family:, tool_proxy: nil, registration_url:, developer_key: nil)
      # make sure the guid never changes
      raise Lti::Errors::InvalidToolProxyError if tool_proxy && tp.tool_proxy_guid != tool_proxy.guid
      raise Errors::InvalidToolProxyError, "Developer key mismatch" if developer_key_mismatch?(tp, developer_key)

      tool_proxy ||= ToolProxy.new
      tool_proxy.registration_url = registration_url
      tool_proxy.product_family = product_family
      tool_proxy.guid = tp.tool_proxy_guid
      tool_proxy.shared_secret = create_secret(tp)
      tool_proxy.product_version = tp.tool_profile.product_instance.product_info.product_version
      tool_proxy.lti_version = tp.tool_profile.lti_version
      tool_proxy.name = tp.tool_profile.product_instance.product_info.default_name
      tool_proxy.description = tp.tool_profile.product_instance.product_info.default_description
      tool_proxy.context = context
      tool_proxy.workflow_state ||= "disabled"
      tool_proxy.raw_data = tp.as_json
      tool_proxy.update_payload = nil
      tool_proxy.save!
      tool_proxy
    end

View on GitHub (pinned to 1c9f0bb801)