{"record":{"id":"89e9d9a1ee0dae76","repo":"instructure/canvas-lms","slug":"tool-invalid","errorCode":"tool_invalid","errorMessage":"Tool is invalid","messagePattern":"Tool is invalid","errorType":"error_code","errorClass":"BasicLTI::Errors::InvalidSourceId","httpStatus":null,"severity":"error","filePath":"lib/basic_lti/basic_outcomes.rb","lineNumber":76,"sourceCode":"        InstStatsd::Statsd.distributed_increment(\"lti.1_1.basic_outcomes.bad_requests\",\n                                                 tags: { error_code: \"InvalidRequest\" })\n        super\n      end\n\n      def response_status\n        415\n      end\n    end\n\n    # gives instfs about 7 hours to have an outage and eventually take the file\n    MAX_ATTEMPTS = 10\n\n    SOURCE_ID_REGEX = /^(\\d+)-(\\d+)-(\\d+)-(\\d+)-(\\w+)$/\n\n    def self.decode_source_id(tool, sourceid)\n      tool.shard.activate do\n        sourcedid = BasicLTI::Sourcedid.load!(sourceid)\n        raise BasicLTI::Errors::InvalidSourceId.new(\"Tool is invalid\", :tool_invalid) unless tool == sourcedid.tool\n\n        return sourcedid.assignment, sourcedid.user\n      end\n    end\n\n    def self.process_request(tool, xml)\n      InstStatsd::Statsd.time(\"lti.1_1.basic_outcomes.process_request_time\") do\n        res = (quizzes_next_tool?(tool) ? BasicLTI::QuizzesNextLtiResponse : LtiResponse).new(xml)\n\n        unless res.handle_request(tool)\n          res.code_major = \"unsupported\"\n          res.description = \"Request could not be handled. ¯\\\\_(ツ)_/¯\"\n        end\n        res\n      end\n    end\n\n    def self.quizzes_next_tool?(tool)","sourceCodeStart":58,"sourceCodeEnd":94,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/lib/basic_lti/basic_outcomes.rb#L58-L94","documentation":"BasicLTI::BasicOutcomes.decode_source_id loads the signed sourcedid and compares its embedded tool with the tool the request was authenticated as. If they differ, the request is from a different tool than the one that issued the sourcedid, so InvalidSourceId with code tool_invalid is raised.","triggerScenarios":"A Basic Outcomes (grade replace) XML request authenticated with a different OAuth consumer key/tool than the one encoded in the lis_result_sourcedid; a tool re-keyed or recreated so its id no longer matches; sourcedid copied between tool contexts.","commonSituations":"Shared consumer keys across multiple tool deployments; a tool re-registered with a new key mid-flight so old sourcedids validate against the new tool object; TP forwarding grade requests from another tool's launch.","solutions":["Ensure the tool authenticating the outcomes request is the same one that generated the sourcedid (same consumer key)","Re-issue the LTI launch so the student gets a fresh sourcedid bound to the current tool","Check for duplicated or re-keyed ContextExternalTool records and consolidate them","Verify the TP is not replaying sourcedids captured from another tool's launches"],"exampleFix":"// before\ntool = ContextExternalTool.find_by(consumer_key: shared_key) // may differ from sourcedid tool\n// after\ntool = BasicLTI::Sourcedid.load!(sourceid).tool // use the sourcedid's own tool","handlingStrategy":"try-catch","validationCode":"sourcedid = BasicLTI::Sourcedid.load!(sourceid)\nraise \"tool mismatch\" unless sourcedid.tool.id == tool.id","typeGuard":null,"tryCatchPattern":"begin\n  assignment, user = BasicLTI::BasicOutcomes.decode_source_id(tool, sourceid)\nrescue BasicLTI::Errors::InvalidSourceId => e\n  return xml failure response for code e.code\nend","preventionTips":["Authenticate the outcomes request with the same consumer key that issued the sourcedid","Avoid recreating/re-keying tools mid-lifecycle","Return a typed failure (tool_invalid) to the TP instead of a 500"],"tags":["lti","basic-outcomes","security","sourcedid"],"backgroundTag":"invalid-identifier","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}