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
- Issue a fresh token (new nonce) for each logout service invocation
- Check callbacks.keys before invoking, or rescue BasicLTI::BasicOutcomes::Unauthorized to treat replay as a no-op
- 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
- Never reuse a logout service token
- Generate a fresh nonce per token
- Handle replay idempotently server-side
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
- Invalid nonce claim in ID Token
- Access token expired
- Access token invalid - signature likely incorrect
- assignment_invalid
- Assignment not configured for external tool launches
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)