{"record":{"id":"8624d53708cdebc8","repo":"instructure/canvas-lms","slug":"failed-to-decode-openid-connect-back-channel-logout-token","errorCode":null,"errorMessage":"Failed to decode OpenID Connect back-channel logout token: #{jwt_string.inspect}","messagePattern":"Failed to decode OpenID Connect back-channel logout token: #(.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/controllers/login/openid_connect_controller.rb","lineNumber":63,"sourceCode":"    unless Pseudonym.expire_oidc_session(logout_token, request)\n      increment_statsd(:failure, reason: :no_session)\n      return render plain: \"No session found\", status: :not_found\n    end\n\n    increment_statsd(:success)\n    render plain: \"OK\", status: :ok\n  end\n\n  def validate_logout_token\n    # NOT SUPPORTED without redis\n    return render plain: \"NOT SUPPORTED\", status: :method_not_allowed unless Canvas.redis_enabled?\n\n    jwt_string = request.request_parameters[\"logout_token\"]\n\n    logout_token = begin\n      ::Canvas::Security.decode_jwt(jwt_string, [:skip_verification])\n    rescue ::Canvas::Security::InvalidToken\n      Rails.logger.warn(\"Failed to decode OpenID Connect back-channel logout token: #{jwt_string.inspect}\")\n      render plain: \"Invalid logout token\", status: :bad_request\n      increment_statsd(:failure, reason: :invalid_token)\n      nil\n    end\n    return unless logout_token\n\n    unless (missing_claims = %w[aud iss iat exp events jti] - logout_token.keys).empty?\n      render plain: \"Missing claim#{\"s\" if missing_claims.length > 1} #{missing_claims.join(\", \")}\",\n             status: :bad_request\n      increment_statsd(:failure, reason: :missing_claims)\n      return\n    end\n    unless logout_token[\"events\"].is_a?(Hash) && logout_token[\"events\"][OIDC_BACKCHANNEL_LOGOUT_EVENT_URN].is_a?(Hash)\n      render plain: \"Invalid events\", status: :bad_request\n      increment_statsd(:failure, reason: :invalid_events)\n      return\n    end\n    unless logout_token[\"sid\"] || logout_token[\"sub\"]","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/controllers/login/openid_connect_controller.rb#L45-L81","documentation":"OpenID Connect back-channel logout sends a signed `logout_token` JWT. Canvas decodes it with verification skipped just to inspect claims; if the string is not a structurally decodable JWT, Canvas::Security::InvalidToken is raised, the controller logs this warning, renders 400 'Invalid logout token', and increments an :invalid_token failure stat.","triggerScenarios":"POST to the OpenID Connect back-channel logout endpoint where params['logout_token'] is missing, empty, truncated, or not a base64url JWS (decode_jwt with [:skip_verification] still requires valid JWT structure).","commonSituations":"IdP misconfigured to send an encrypted JWE the decoder can't parse; token truncated by a proxy; client posting the token under the wrong param name; non-OP callers hitting the endpoint.","solutions":["Verify the IdP sends an unencrypted signed JWS logout_token (or configure decryption if your setup supports JWE)","Inspect jwt_string in the log — confirm the param is populated and not truncated by proxies or request limits","Confirm the client posts logout_token as a form param on the back-channel logout URL","Check Canvas::Security key configuration (JWK/signing keys) matches the issuer"],"exampleFix":"// before\ncurl -X POST $HOST/login/openid_connect/logout -d 'token=...'\n// after\ncurl -X POST $HOST/login/openid_connect/logout -d 'logout_token=eyJhbGciOi...'","handlingStrategy":"validation","validationCode":"logout_token = params['logout_token']\nvalid = logout_token.present? && logout_token.split('.').length == 3","typeGuard":"def jwt_like?(str)\n  str.is_a?(String) && str.split('.').length == 3 && str.match?(/\\A[\\w-]+\\.[\\w-]+\\.[\\w-]*\\z/)\nend","tryCatchPattern":"begin\n  token = Canvas::Security.decode_jwt(jwt_string, [:skip_verification])\nrescue Canvas::Security::InvalidToken\n  render plain: 'Invalid logout token', status: :bad_request\nend","preventionTips":["Ensure the IdP back-channel logout posts logout_token as a form param","Confirm the IdP emits signed JWS (not encrypted JWE) tokens","Check proxies don't truncate large POST bodies","Log token length/shape (not content) for diagnosis"],"tags":["openid-connect","jwt","logout","sso"],"backgroundTag":"invalid-argument-format","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"}