From 89feb325b7663de92d6f48cfdc4e7ccde6738c06 Mon Sep 17 00:00:00 2001 From: Claude Vervoort Date: Tue, 22 Jun 2021 17:31:04 -0400 Subject: [PATCH] MDL-71887 mod_lti: repost when no cookie due to crosssite request --- mod/lti/auth.php | 13 ++++ mod/lti/classes/output/renderer.php | 13 ++++ .../classes/output/repost_crosssite_page.php | 76 +++++++++++++++++++ mod/lti/contentitem_return.php | 26 ++++++- mod/lti/templates/repost_crosssite.mustache | 49 ++++++++++++ 5 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 mod/lti/classes/output/repost_crosssite_page.php create mode 100644 mod/lti/templates/repost_crosssite.mustache diff --git a/mod/lti/auth.php b/mod/lti/auth.php index 831b6315a3a..50cc0b304eb 100644 --- a/mod/lti/auth.php +++ b/mod/lti/auth.php @@ -24,6 +24,19 @@ require_once(__DIR__ . '/../../config.php'); require_once($CFG->dirroot . '/mod/lti/locallib.php'); +global $_POST, $_SERVER; + +if (!isloggedin() && empty($_POST['repost'])) { + header_remove("Set-Cookie"); + $PAGE->set_pagelayout('popup'); + $PAGE->set_context(context_system::instance()); + $output = $PAGE->get_renderer('mod_lti'); + $page = new \mod_lti\output\repost_crosssite_page($_SERVER['REQUEST_URI'], $_POST); + echo $output->header(); + echo $output->render($page); + echo $output->footer(); + return; +} $scope = optional_param('scope', '', PARAM_TEXT); $responsetype = optional_param('response_type', '', PARAM_TEXT); diff --git a/mod/lti/classes/output/renderer.php b/mod/lti/classes/output/renderer.php index b94178da9f7..f2868470264 100644 --- a/mod/lti/classes/output/renderer.php +++ b/mod/lti/classes/output/renderer.php @@ -59,4 +59,17 @@ class renderer extends plugin_renderer_base { $data = $page->export_for_template($this); return parent::render_from_template('mod_lti/external_registration_return', $data); } + + /** + * Render the reposting of the cross site request. + * + * @param repost_crosssite_page $page the page renderable. + * + * @return string rendered html for the page. + */ + public function render_repost_crosssite_page(repost_crosssite_page $page): string { + $data = $page->export_for_template($this); + return parent::render_from_template('mod_lti/repost_crosssite', $data); + } + } diff --git a/mod/lti/classes/output/repost_crosssite_page.php b/mod/lti/classes/output/repost_crosssite_page.php new file mode 100644 index 00000000000..e7c3501b52e --- /dev/null +++ b/mod/lti/classes/output/repost_crosssite_page.php @@ -0,0 +1,76 @@ +. + +/** + * Render a page containing a simple form which reposts to self via JS. + * + * The purpose of this form is to resend a cross-site request to self, which allows the browsers to include the Moodle + * session cookie alongside the original POST data, allowing LTI flows to function despite browsers blocking + * cross-site cookies. + * + * @copyright 2021 Cengage + * @package mod_lti + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +namespace mod_lti\output; + +defined('MOODLE_INTERNAL') || die; + +require_once($CFG->dirroot.'/mod/lti/locallib.php'); + +use renderable; +use templatable; +use renderer_base; +use stdClass; + +/** + * Render a page containing a simple form which reposts to self via JS. + * + * The purpose of this form is to resend a cross-site request to self, which allows the browsers to include the Moodle + * session cookie alongside the original POST data, allowing LTI flows to function despite browsers blocking + * cross-site cookies. + * + * @copyright 2021 Cengage + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class repost_crosssite_page implements renderable, templatable { + + /** + * Constructor + * + * @param string $url moodle URL to repost to + * @param array $post the POST params to be re-posted + */ + public function __construct(string $url, array $post) { + $this->params = array_map(function($k) use ($post) { + return ["key" => $k, "value" => str_replace("\"", """, $post[$k])]; + }, array_keys($post)); + $this->url = $url; + } + + /** + * Export this data so it can be used as the context for a mustache template. + * + * @param renderer_base $output The renderer + * @return stdClass Data to be used by the template + */ + public function export_for_template(renderer_base $output) { + $renderdata = new stdClass(); + $renderdata->url = $this->url; + $renderdata->params = $this->params; + return $renderdata; + } +} diff --git a/mod/lti/contentitem_return.php b/mod/lti/contentitem_return.php index 173e26ec864..af819dc1997 100644 --- a/mod/lti/contentitem_return.php +++ b/mod/lti/contentitem_return.php @@ -31,6 +31,28 @@ $courseid = required_param('course', PARAM_INT); $jwt = optional_param('JWT', '', PARAM_RAW); +$context = context_course::instance($courseid); + +$pageurl = new moodle_url('/mod/lti/contentitem_return.php'); +$PAGE->set_url($pageurl); +$PAGE->set_pagelayout('popup'); +$PAGE->set_context($context); + +// Cross-Site causes the cookie to be lost if not POSTed from same site. +global $_POST; +if (!empty($_POST["repost"])) { + // Unset the param so that LTI 1.1 signature validation passes. + unset($_POST["repost"]); +} else if (!isloggedin()) { + header_remove("Set-Cookie"); + $output = $PAGE->get_renderer('mod_lti'); + $page = new \mod_lti\output\repost_crosssite_page($_SERVER['REQUEST_URI'], $_POST); + echo $output->header(); + echo $output->render($page); + echo $output->footer(); + return; +} + if (!empty($jwt)) { $params = lti_convert_from_jwt($id, $jwt); $consumerkey = $params['oauth_consumer_key'] ?? ''; @@ -52,7 +74,6 @@ if (!empty($jwt)) { $course = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST); require_login($course); require_sesskey(); -$context = context_course::instance($courseid); require_capability('moodle/course:manageactivities', $context); require_capability('mod/lti:addcoursetool', $context); @@ -66,9 +87,6 @@ if (empty($errormsg) && !empty($items)) { } } -$pageurl = new moodle_url('/mod/lti/contentitem_return.php'); -$PAGE->set_url($pageurl); -$PAGE->set_pagelayout('popup'); echo $OUTPUT->header(); // Call JS module to redirect the user to the course page or close the dialogue on error/cancel. diff --git a/mod/lti/templates/repost_crosssite.mustache b/mod/lti/templates/repost_crosssite.mustache new file mode 100644 index 00000000000..84cacbc32b9 --- /dev/null +++ b/mod/lti/templates/repost_crosssite.mustache @@ -0,0 +1,49 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template mod_lti/repost_crosssite + + This template re-executes a cross site request but from the site domain, avoiding the cross site issue. + + Classes required for JS: + * none + + Data attributes required for JS: + * none + + Context variables required for this template: + * + + Example context (json): + { + "url":"https://some.tool.example/mod/lti/auth.php", + "params": { + "response_type": "id_token" + } + } + +}} +
+ {{#params}} + + {{/params}} + +
+ +{{#js}} + document.getElementById("autopostme").submit(); +{{/js}}