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

Invalid logout service token

Error message

Invalid logout service token

What it means

LogoutService.parse_and_validate deserializes a signed, hyphen-joined token containing tool id, pseudonym id, timestamp, nonce and an HMAC-SHA1 signature computed with the tool shard's encryption_key. It raises BasicLTI::BasicOutcomes::Unauthorized when the token doesn't have exactly 5 parts or the recomputed HMAC does not match the final part.

Solutions

  1. Verify the tool shard settings[:encryption_key] is the same key used when the token was generated
  2. Regenerate the logout service token after rotating the encryption key
  3. Check the serialized token has exactly 5 hyphen-separated parts with no stray hyphens in payload values
  4. Rescue BasicLTI::BasicOutcomes::Unauthorized and reject the logout request with 401
Defensive patterns

Strategy: try-catch

Validate before calling

valid = token.count("-") == 4 # 5 parts expected

Try / catch

begin
  token = Lti::LogoutService.parse_and_validate(serialized)
rescue BasicLTI::BasicOutcomes::Unauthorized
  head :unauthorized
end

Prevention

When it happens

Trigger: Passing a token with wrong number of segments, or a token whose signature was computed with a different/outdated encryption key, or a tampered token, to LogoutService.parse_and_validate.

Common situations: Tool shard encryption_key rotated after token issuance; tokens copied across environments/shards; hand-built or truncated tokens; different shard settings lacking :encryption_key.

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/3e0091d3a3bdf209. Report an issue: GitHub.

Appendix: source

Thrown at app/models/lti/logout_service.rb:53

    end

    Token = Struct.new(:tool, :pseudonym, :timestamp, :nonce) do
      def self.create(tool, pseudonym)
        Token.new(tool, pseudonym, Time.zone.now, SecureRandom.hex(8))
      end

      def serialize
        key = tool.shard.settings[:encryption_key]
        payload = [tool.id, pseudonym.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 == 5 && Canvas::Security.hmac_sha1(parts[0..-2].join("-"), key) == parts[-1]
          raise BasicLTI::BasicOutcomes::Unauthorized, "Invalid logout service token"
        end

        pseudonym = Pseudonym.find(parts[1].to_i)
        timestamp = parts[2].to_i
        nonce = parts[3]
        unless Time.now.to_i - timestamp < Lti::LogoutService::TOKEN_EXPIRATION
          raise BasicLTI::BasicOutcomes::Unauthorized, "Logout service token has expired"
        end

        Token.new(tool, pseudonym, timestamp, nonce)
      end
    end

    Runner = Struct.new(:callbacks) do
      def perform
        callbacks.each_value do |callback|
          InstrumentTLSCiphers.without_tls_metrics do
            CanvasHttp.get(URI.parse(callback).to_s)

View on GitHub (pinned to 1c9f0bb801)