{"record":{"id":"6e79820ae687a064","repo":"instructure/canvas-lms","slug":"user-not-in-course","errorCode":"user_not_in_course","errorMessage":"User is no longer in course","messagePattern":"User is no longer in course","errorType":"error_code","errorClass":"BasicLTI::Errors::InvalidSourceId","httpStatus":null,"severity":"error","filePath":"lib/basic_lti/sourcedid.rb","lineNumber":55,"sourceCode":"      Canvas::Security.base64_encode(crypted_token)\n    end\n\n    def jwt_payload\n      {\n        iss: \"Canvas\",\n        aud: [\"Instructure\"],\n        iat: Time.zone.now.to_i,\n        tool_id: tool.id,\n        course_id: course.id,\n        assignment_id: assignment.id,\n        user_id: user.id,\n      }\n    end\n    private :jwt_payload\n\n    def validate!\n      raise Errors::InvalidSourceId.new(\"Course is invalid\", :course_invalid) unless course\n      raise Errors::InvalidSourceId.new(\"User is no longer in course\", :user_not_in_course) unless user\n      raise Errors::InvalidSourceId.new(\"Assignment is invalid\", :assignment_invalid) unless assignment\n\n      tag = assignment.external_tool_tag\n      raise Errors::InvalidSourceId.new(\"Assignment is no longer associated with this tool\", :assignment_tool_mismatch) unless tag &&\n                                                                                                                               (tool.matches_url?(tag.url, match_queries_exactly: false) || tool.matches_tool_domain?(tag.url)) &&\n                                                                                                                               tool.workflow_state != \"deleted\"\n    end\n\n    def self.load!(sourcedid_string)\n      raise Errors::InvalidSourceId.new(\"Invalid sourcedid\", :sourcedid_invalid) if sourcedid_string.blank?\n\n      token = load_from_legacy_sourcedid!(sourcedid_string) ||\n              token_from_sourcedid!(sourcedid_string)\n\n      tool = Lti::ToolFinder.find_by(id: token[:tool_id])\n      course = Course.active.find_by(id: token[:course_id])\n      if course\n        user = course.student_enrollments.active.find_by(user_id: token[:user_id])&.user","sourceCodeStart":37,"sourceCodeEnd":73,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/lib/basic_lti/sourcedid.rb#L37-L73","documentation":"BasicLti::Sourcedid#validate! re-resolves the LTI launch context from the sourcedid token. It raises Errors::InvalidSourceId with code :user_not_in_course when the user lookup returns nil, meaning the enrollment that originally authorized this launch no longer exists or is not active. The library throws it to prevent further LTI API operations (grades, returns) on behalf of a user who can no longer access the course.","triggerScenarios":"Calling Sourcedid.load!(sourcedid) (which runs validate!) when token[:user_id] does not resolve to a user with an active enrollment in the course — e.g. the student was unenrolled or the enrollment was concluded/deleted after the launch.","commonSituations":"A student dropped the course and the tool later calls grade passback or a course-navigation return using the stored sourcedid; batch grade synchronization with stale lis_result_sourcedid values; a course copy/cleanup that removed enrollments.","solutions":["Re-launch the tool to obtain a fresh sourcedid for the current enrollment.","Check the user's enrollment state in the course (Enrollment.active.where(user_id:, course_id:)) before sending grades with the old sourcedid.","If the user was removed intentionally, stop using the stored sourcedid and drop it from your sync queue.","If this is unexpected, verify the user_id in the sourcedid token matches the intended student and that no enrollment cleanup job deleted it."],"exampleFix":"// before\nsub = BasicLti::Sourcedid.load!(sourcedid) # raises if student unenrolled\nsub.update_submission_score(score)\n// after\nbegin\n  sub = BasicLti::Sourcedid.load!(sourcedid)\n  sub.update_submission_score(score)\nrescue BasicLti::Sourcedid::Errors::InvalidSourceId => e\n  Rails.logger.warn(\"skipping stale sourcedid: #{e.message}\") if e.error_code == :user_not_in_course\nend","handlingStrategy":"validation","validationCode":"# ruby\nvalid = Enrollment.active.where(user_id: user_id, course_id: course_id).exists?\nraise \"stale sourcedid: user not enrolled\" unless valid","typeGuard":null,"tryCatchPattern":"begin\n  BasicLti::Sourcedid.load!(sourcedid)\nrescue BasicLti::Sourcedid::Errors::InvalidSourceId => e\n  handle_stale_sourcedid if e.error_code == :user_not_in_course\nend","preventionTips":["Listen for enrollment-deleted/concluded events and invalidate stored sourcedids.","Re-fetch sourcedids via fresh launches instead of long-term storage.","Skip-and-log stale sourcedids in grade sync jobs instead of failing the whole batch."],"tags":["lti","enrollment","stale-token"],"backgroundTag":"record-not-found","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"}