instructure/canvas-lms · error · BasicLTI::Errors::InvalidSourceId

user_not_in_course

user_not_in_course

Error message

User is no longer in course

What it means

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.

Solutions

  1. Re-launch the tool to obtain a fresh sourcedid for the current enrollment.
  2. Check the user's enrollment state in the course (Enrollment.active.where(user_id:, course_id:)) before sending grades with the old sourcedid.
  3. If the user was removed intentionally, stop using the stored sourcedid and drop it from your sync queue.
  4. If this is unexpected, verify the user_id in the sourcedid token matches the intended student and that no enrollment cleanup job deleted it.

Example fix

// before
sub = BasicLti::Sourcedid.load!(sourcedid) # raises if student unenrolled
sub.update_submission_score(score)
// after
begin
  sub = BasicLti::Sourcedid.load!(sourcedid)
  sub.update_submission_score(score)
rescue BasicLti::Sourcedid::Errors::InvalidSourceId => e
  Rails.logger.warn("skipping stale sourcedid: #{e.message}") if e.error_code == :user_not_in_course
end
Defensive patterns

Strategy: validation

Validate before calling

# ruby
valid = Enrollment.active.where(user_id: user_id, course_id: course_id).exists?
raise "stale sourcedid: user not enrolled" unless valid

Try / catch

begin
  BasicLti::Sourcedid.load!(sourcedid)
rescue BasicLti::Sourcedid::Errors::InvalidSourceId => e
  handle_stale_sourcedid if e.error_code == :user_not_in_course
end

Prevention

When it happens

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

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

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


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

Appendix: source

Thrown at lib/basic_lti/sourcedid.rb:55

      Canvas::Security.base64_encode(crypted_token)
    end

    def jwt_payload
      {
        iss: "Canvas",
        aud: ["Instructure"],
        iat: Time.zone.now.to_i,
        tool_id: tool.id,
        course_id: course.id,
        assignment_id: assignment.id,
        user_id: user.id,
      }
    end
    private :jwt_payload

    def validate!
      raise Errors::InvalidSourceId.new("Course is invalid", :course_invalid) unless course
      raise Errors::InvalidSourceId.new("User is no longer in course", :user_not_in_course) unless user
      raise Errors::InvalidSourceId.new("Assignment is invalid", :assignment_invalid) unless assignment

      tag = assignment.external_tool_tag
      raise Errors::InvalidSourceId.new("Assignment is no longer associated with this tool", :assignment_tool_mismatch) unless tag &&
                                                                                                                               (tool.matches_url?(tag.url, match_queries_exactly: false) || tool.matches_tool_domain?(tag.url)) &&
                                                                                                                               tool.workflow_state != "deleted"
    end

    def self.load!(sourcedid_string)
      raise Errors::InvalidSourceId.new("Invalid sourcedid", :sourcedid_invalid) if sourcedid_string.blank?

      token = load_from_legacy_sourcedid!(sourcedid_string) ||
              token_from_sourcedid!(sourcedid_string)

      tool = Lti::ToolFinder.find_by(id: token[:tool_id])
      course = Course.active.find_by(id: token[:course_id])
      if course
        user = course.student_enrollments.active.find_by(user_id: token[:user_id])&.user

View on GitHub (pinned to 1c9f0bb801)