instructure/canvas-lms · error · BasicLTI::BasicOutcomes::Unauthorized

Invalid analytics service token

Error message

Invalid analytics service token

What it means

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.

Solutions

  1. Regenerate the token via the analytics service's token-building method so the HMAC is computed with the current encryption_key.
  2. Verify shard.settings[:encryption_key] is present and unchanged since the token was issued; restore/re-rotate consistently.
  3. Ensure the full, unmodified token is passed (all 6 parts) — check for URL truncation of trailing dashes/parts.
  4. Confirm the token is used on the same shard/environment where it was created.

Example fix

// before
token = "1-42-101-1690000000-abc"  # truncated: signature missing
// after
token = build_analytics_token(tool, user, course, timestamp, nonce)  # 6 parts incl. valid hmac
Defensive patterns

Strategy: try-catch

Validate before calling

parts = token.split("-")
raise BasicLTI::BasicOutcomes::Unauthorized if parts.size != 6
tool = Lti::ToolFinder.find(parts[0].to_i)
key = tool.shard.settings[:encryption_key]
raise BasicLTI::BasicOutcomes::Unauthorized if Canvas::Security.hmac_sha1(parts[0..-2].join("-"), key) != parts[-1]

Try / catch

begin
  parsed = Lti::AnalyticsService.parse_and_validate(token)
rescue BasicLTI::BasicOutcomes::Unauthorized
  render json: {error: "invalid token"}, status: :unauthorized
end

Prevention

When it happens

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

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

Understand the failure class

Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.

Related errors


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

Appendix: source

Thrown at app/models/lti/analytics_service.rb:40

module Lti
  class AnalyticsService
    Token = Struct.new(:tool, :user, :course, :timestamp, :nonce) do
      def self.create(tool, user, course)
        Token.new(tool, user, course, Time.zone.now, SecureRandom.hex(8))
      end

      def serialize
        key = tool.shard.settings[:encryption_key]
        payload = [tool.id, user.id, course.id, timestamp.to_i, nonce].join("-")
        "#{payload}-#{Canvas::Security.hmac_sha1(payload, key)}"
      end

      def self.parse_and_validate(serialized_token)
        parts = serialized_token.split("-")
        tool = Lti::ToolFinder.find(parts[0].to_i)
        key = tool.shard.settings[:encryption_key]
        unless parts.size == 6 && Canvas::Security.hmac_sha1(parts[0..-2].join("-"), key) == parts[-1]
          raise BasicLTI::BasicOutcomes::Unauthorized, "Invalid analytics service token"
        end

        user = User.find(parts[1].to_i)
        course = Course.find(parts[2].to_i)
        timestamp = parts[3].to_i
        nonce = parts[4]
        Token.new(tool, user, course, timestamp, nonce)
      end
    end

    def self.create_token(tool, user, course)
      Token.create(tool, user, course).serialize
    end

    def self.log_page_view(token, opts = {})
      course = token.course
      user = token.user
      tool = token.tool

View on GitHub (pinned to 1c9f0bb801)