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
- 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.
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
- 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
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
- Invalid logout service token
- Developer key mismatch
- invalid_grant
- signature_invalid
- the Developer Key is not active or available in this…
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.toolView on GitHub (pinned to 1c9f0bb801)