{"record":{"id":"eed43b3a7acb4102","repo":"instructure/canvas-lms","slug":"logout-service-token-has-expired","errorCode":null,"errorMessage":"Logout service token has expired","messagePattern":"Logout service token has expired","errorType":"http","errorClass":"BasicLTI::BasicOutcomes::Unauthorized","httpStatus":401,"severity":"error","filePath":"app/models/lti/logout_service.rb","lineNumber":60,"sourceCode":"      def serialize\n        key = tool.shard.settings[:encryption_key]\n        payload = [tool.id, pseudonym.id, timestamp.to_i, nonce].join(\"-\")\n        \"#{payload}-#{Canvas::Security.hmac_sha1(payload, key)}\"\n      end\n\n      def self.parse_and_validate(serialized_token)\n        parts = serialized_token.split(\"-\")\n        tool = Lti::ToolFinder.find(parts[0].to_i)\n        key = tool.shard.settings[:encryption_key]\n        unless parts.size == 5 && Canvas::Security.hmac_sha1(parts[0..-2].join(\"-\"), key) == parts[-1]\n          raise BasicLTI::BasicOutcomes::Unauthorized, \"Invalid logout service token\"\n        end\n\n        pseudonym = Pseudonym.find(parts[1].to_i)\n        timestamp = parts[2].to_i\n        nonce = parts[3]\n        unless Time.now.to_i - timestamp < Lti::LogoutService::TOKEN_EXPIRATION\n          raise BasicLTI::BasicOutcomes::Unauthorized, \"Logout service token has expired\"\n        end\n\n        Token.new(tool, pseudonym, timestamp, nonce)\n      end\n    end\n\n    Runner = Struct.new(:callbacks) do\n      def perform\n        callbacks.each_value do |callback|\n          InstrumentTLSCiphers.without_tls_metrics do\n            CanvasHttp.get(URI.parse(callback).to_s)\n          end\n        rescue => e\n          Rails.logger.error(\"Failed to call logout callback '#{callback}': #{e.inspect}\")\n        end\n      end\n    end\n","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/models/lti/logout_service.rb#L42-L78","documentation":"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.","triggerScenarios":"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.","commonSituations":"Long-lived browser sessions where the launch URL is visited hours later; server clock drift; slow background processing of logout callbacks.","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"],"exampleFix":"// before\nLogoutService.parse_and_validate(stale_token) # raises\n// after\nbegin\n  LogoutService.parse_and_validate(token)\nrescue BasicLTI::BasicOutcomes::Unauthorized\n  # request a fresh token from the launch flow\nend","handlingStrategy":"try-catch","validationCode":"fresh = Time.now.to_i - token_timestamp < Lti::LogoutService::TOKEN_EXPIRATION","typeGuard":null,"tryCatchPattern":"begin\n  LogoutService.parse_and_validate(token)\nrescue BasicLTI::BasicOutcomes::Unauthorized\n  redirect_to launch_url # force relaunch for fresh token\nend","preventionTips":["Use tokens promptly after issuance","Keep servers NTP-synced","Rescue Unauthorized and trigger a relaunch flow"],"tags":["lti","token-expiry","ttl"],"backgroundTag":"jwt-token-expired","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}