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
- Ensure the developer_key passed to process_tool_proxy_json matches the key bound to the tool proxy payload / registration request.
- If the key was rotated, redo the tool registration from scratch so the proxy is created under the current key.
- 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.
- 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
- Always derive the developer key from the same registration session that created the proxy
- Redo full registration after key rotation instead of replaying old payloads
- Check for existing tool proxies with the same guid bound to another key
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
- Assignment not configured for external tool launches
- e.message
- Invalid analytics service token
- invalid_grant
- #
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)