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

Logout service token has expired

Error message

Logout service token has expired

What it means

After signature validation, parse_and_validate checks that the token's embedded timestamp is within Lti::LogoutService::TOKEN_EXPIRATION seconds of now. Expired tokens raise BasicLTI::BasicOutcomes::Unauthorized with this message.

Solutions

  1. Obtain a fresh logout service token (re-run the launch flow) and retry
  2. Ensure NTP/clock synchronization across app servers to avoid false expiry
  3. Increase Lti::LogoutService::TOKEN_EXPIRATION if legitimate flows exceed the window
  4. Rescue BasicLTI::BasicOutcomes::Unauthorized and prompt the user/tool to relaunch

Example fix

// before
LogoutService.parse_and_validate(stale_token) # raises
// after
begin
  LogoutService.parse_and_validate(token)
rescue BasicLTI::BasicOutcomes::Unauthorized
  # request a fresh token from the launch flow
end
Defensive patterns

Strategy: try-catch

Validate before calling

fresh = Time.now.to_i - token_timestamp < Lti::LogoutService::TOKEN_EXPIRATION

Try / catch

begin
  LogoutService.parse_and_validate(token)
rescue BasicLTI::BasicOutcomes::Unauthorized
  redirect_to launch_url # force relaunch for fresh token
end

Prevention

When it happens

Trigger: Presenting a logout service token whose timestamp is older than TOKEN_EXPIRATION seconds at validation time — typically a launch that sat too long, queued requests, or clock skew between servers.

Common situations: Long-lived browser sessions where the launch URL is visited hours later; server clock drift; slow background processing of logout callbacks.

Related errors


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

Appendix: source

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

      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)
          end
        rescue => e
          Rails.logger.error("Failed to call logout callback '#{callback}': #{e.inspect}")
        end
      end
    end

View on GitHub (pinned to 1c9f0bb801)