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

Logout service token has already been used

Error message

Logout service token has already been used

What it means

Logout service tokens are single-use: each token's nonce is registered in the Rails cache keyed by pseudonym. register_logout_callback rejects a callback if the token's nonce already exists in the cached callback map, raising Unauthorized, because replaying a nonce means the token was already used.

Solutions

  1. Issue a fresh token (new nonce) for each logout service invocation
  2. Check callbacks.keys before invoking, or rescue BasicLTI::BasicOutcomes::Unauthorized to treat replay as a no-op
  3. Clear/expire the pseudonym's logout callback cache entry only through legitimate logout completion, not by replaying old tokens

Example fix

// before
LogoutService.register_logout_callback(reused_token, cb) # raises
// after
token = LogoutService.create_token(tool, pseudonym) # fresh nonce each time
LogoutService.register_logout_callback(token, cb)
Defensive patterns

Strategy: try-catch

Validate before calling

used = Lti::LogoutService.get_logout_callbacks(token.pseudonym).key?(token.nonce)
raise "token replay" if used

Try / catch

begin
  LogoutService.register_logout_callback(token, callback)
rescue BasicLTI::BasicOutcomes::Unauthorized
  # token already consumed; treat as no-op
end

Prevention

When it happens

Trigger: Calling register_logout_callback twice with tokens carrying the same nonce for the same pseudonym — e.g. replaying the same logout service token, or nonce collisions from a badly generated token.

Common situations: Tools retrying logout service calls with the same token; cache retaining the nonce for the 1-day expiry window; token generation reusing nonces.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


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

Appendix: source

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

        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

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

    def self.register_logout_callback(token, callback)
      return unless token.pseudonym&.id && callback.present?

      callbacks = get_logout_callbacks(token.pseudonym)
      raise BasicLTI::BasicOutcomes::Unauthorized, "Logout service token has already been used" if callbacks.key?(token.nonce)

      callbacks[token.nonce] = callback
      Rails.cache.write(cache_key(token.pseudonym), callbacks, expires_in: 1.day)
    end

    def self.queue_callbacks(pseudonym)
      return unless pseudonym&.id

      callbacks = get_logout_callbacks(pseudonym)
      return unless callbacks.any?

      clear_logout_callbacks(pseudonym)
      Delayed::Job.enqueue(Lti::LogoutService::Runner.new(callbacks))
    end
  end
end

View on GitHub (pinned to 1c9f0bb801)