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
- Obtain a fresh logout service token (re-run the launch flow) and retry
- Ensure NTP/clock synchronization across app servers to avoid false expiry
- Increase Lti::LogoutService::TOKEN_EXPIRATION if legitimate flows exceed the window
- 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
- Use tokens promptly after issuance
- Keep servers NTP-synced
- Rescue Unauthorized and trigger a relaunch flow
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
- Access token expired
- Access token invalid - signature likely incorrect
- assignment_invalid
- Assignment not configured for external tool launches
- Assignment not configured for launches with specified tool
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)