{"record":{"id":"38bf6a2fa73c9b1f","repo":"instructure/canvas-lms","slug":"invalid-analytics-service-token","errorCode":null,"errorMessage":"Invalid analytics service token","messagePattern":"Invalid analytics service token","errorType":"http","errorClass":"BasicLTI::BasicOutcomes::Unauthorized","httpStatus":401,"severity":"error","filePath":"app/models/lti/analytics_service.rb","lineNumber":40,"sourceCode":"module Lti\n  class AnalyticsService\n    Token = Struct.new(:tool, :user, :course, :timestamp, :nonce) do\n      def self.create(tool, user, course)\n        Token.new(tool, user, course, Time.zone.now, SecureRandom.hex(8))\n      end\n\n      def serialize\n        key = tool.shard.settings[:encryption_key]\n        payload = [tool.id, user.id, course.id, timestamp.to_i, nonce].join(\"-\")\n        \"#{payload}-#{Canvas::Security.hmac_sha1(payload, key)}\"\n      end\n\n      def self.parse_and_validate(serialized_token)\n        parts = serialized_token.split(\"-\")\n        tool = Lti::ToolFinder.find(parts[0].to_i)\n        key = tool.shard.settings[:encryption_key]\n        unless parts.size == 6 && Canvas::Security.hmac_sha1(parts[0..-2].join(\"-\"), key) == parts[-1]\n          raise BasicLTI::BasicOutcomes::Unauthorized, \"Invalid analytics service token\"\n        end\n\n        user = User.find(parts[1].to_i)\n        course = Course.find(parts[2].to_i)\n        timestamp = parts[3].to_i\n        nonce = parts[4]\n        Token.new(tool, user, course, timestamp, nonce)\n      end\n    end\n\n    def self.create_token(tool, user, course)\n      Token.create(tool, user, course).serialize\n    end\n\n    def self.log_page_view(token, opts = {})\n      course = token.course\n      user = token.user\n      tool = token.tool","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/lti/analytics_service.rb#L22-L58","documentation":"Lti::AnalyticsService.parse_and_validate splits a serialized token on '-', looks up the tool by parts[0], derives the tool shard's encryption_key, and verifies an HMAC-SHA1 of all parts except the last against the trailing signature. If the part count is not 6 or the HMAC does not match, it raises BasicLTI::BasicOutcomes::Unauthorized — the token is forged, truncated, or generated with a different key.","triggerScenarios":"Presenting a serialized analytics token where: it doesn't split into exactly 6 dash-separated parts; the HMAC signature (last part) doesn't match hmac_sha1 of the prefix with the tool's shard :encryption_key; or the tool id in parts[0] resolves to a different shard/key than the one that signed it.","commonSituations":"Tokens minted before an encryption_key rotation; hand-edited or truncated tokens in URLs; copying tokens between environments (test vs production shards); missing :encryption_key in shard settings.","solutions":["Regenerate the token via the analytics service's token-building method so the HMAC is computed with the current encryption_key.","Verify shard.settings[:encryption_key] is present and unchanged since the token was issued; restore/re-rotate consistently.","Ensure the full, unmodified token is passed (all 6 parts) — check for URL truncation of trailing dashes/parts.","Confirm the token is used on the same shard/environment where it was created."],"exampleFix":"// before\ntoken = \"1-42-101-1690000000-abc\"  # truncated: signature missing\n// after\ntoken = build_analytics_token(tool, user, course, timestamp, nonce)  # 6 parts incl. valid hmac","handlingStrategy":"try-catch","validationCode":"parts = token.split(\"-\")\nraise BasicLTI::BasicOutcomes::Unauthorized if parts.size != 6\ntool = Lti::ToolFinder.find(parts[0].to_i)\nkey = tool.shard.settings[:encryption_key]\nraise BasicLTI::BasicOutcomes::Unauthorized if Canvas::Security.hmac_sha1(parts[0..-2].join(\"-\"), key) != parts[-1]","typeGuard":null,"tryCatchPattern":"begin\n  parsed = Lti::AnalyticsService.parse_and_validate(token)\nrescue BasicLTI::BasicOutcomes::Unauthorized\n  render json: {error: \"invalid token\"}, status: :unauthorized\nend","preventionTips":["Never hand-edit or truncate serialized tokens","Re-issue tokens after any encryption_key rotation","Pass tokens between environments only with the matching shard key"],"tags":["lti","hmac","authentication","token-validation"],"backgroundTag":"checksum-mismatch","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"}